diff --git a/build/build.xml b/build/build.xml index d53af671..e0147c40 100644 --- a/build/build.xml +++ b/build/build.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/build/packageManifest.xml b/build/packageManifest.xml index a8cef849..25bf7037 100644 --- a/build/packageManifest.xml +++ b/build/packageManifest.xml @@ -336,8 +336,8 @@ https://umbraco.com 7 - 6 - 4 + 7 + 0 @@ -347,7 +347,7 @@ +It contains a simple website that contains many basic features to help get you started including a home page, a blog, a product catalogue, contact page and more.]]> @@ -1577,7 +1577,7 @@ It's only compatible with Umbraco 7.6.4+, it will not install or work on older v Features features - Our.Umbraco.NestedContent + Umbraco.NestedContent 2b806d03-d9f7-41a3-8226-4abc870585c4 Features 0 @@ -2283,7 +2283,7 @@ It's only compatible with Umbraco 7.6.4+, it will not install or work on older v - + diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index c78eb517..f9ea9216 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,6 +11,6 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -[assembly: AssemblyVersion("1.0.2")] -[assembly: AssemblyFileVersion("1.0.2")] -[assembly: AssemblyInformationalVersion("1.0.2")] +[assembly: AssemblyVersion("2.0.0")] +[assembly: AssemblyFileVersion("2.0.0")] +[assembly: AssemblyInformationalVersion("2.0.0")] diff --git a/src/Umbraco.SampleSite.Website/App_Data/Umbraco.sdf b/src/Umbraco.SampleSite.Website/App_Data/Umbraco.sdf index ab047626..ec5381ad 100644 Binary files a/src/Umbraco.SampleSite.Website/App_Data/Umbraco.sdf and b/src/Umbraco.SampleSite.Website/App_Data/Umbraco.sdf differ diff --git a/src/Umbraco.SampleSite.Website/App_Data/packages/created/createdPackages.config b/src/Umbraco.SampleSite.Website/App_Data/packages/created/createdPackages.config index a5af17b4..6fe04a6d 100644 --- a/src/Umbraco.SampleSite.Website/App_Data/packages/created/createdPackages.config +++ b/src/Umbraco.SampleSite.Website/App_Data/packages/created/createdPackages.config @@ -1,13 +1,11 @@  - + MIT License Umbraco HQ - +It contains a simple website that contains many basic features to help get you started including a home page, a blog, a product catalogue, contact page and more.]]> @@ -26,7 +24,7 @@ Todo: - 1090,1180,1181,1093,1094,1182,1096,1177,1183,1098,1099,1100,1101,1102,1103,1104,1184,1185,1187,1174 + 1104,1096,1174,1177,1093,1101,1180,1098,1100,1090,1181,1099,1187,1183,1182,1102,1185,1103,1094,1184 1105,1106,1107,1108,1109,1110,1111,1112,1113,1114 @@ -55,8 +53,6 @@ Todo: /Views/MacroPartials/FeaturedProducts.cshtml /Views/MacroPartials/LatestBlogposts.cshtml /Views/Partials/Navigation - /bin/Our.Umbraco.NestedContent.dll - /App_Plugins/NestedContent /bin/Umbraco.SampleSite.dll /App_Plugins/Terratype /App_Plugins/Terratype.GoogleMapsV3 diff --git a/src/Umbraco.SampleSite.Website/Umbraco.SampleSite.Website.csproj b/src/Umbraco.SampleSite.Website/Umbraco.SampleSite.Website.csproj index b7fe2e75..09ea1d3a 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco.SampleSite.Website.csproj +++ b/src/Umbraco.SampleSite.Website/Umbraco.SampleSite.Website.csproj @@ -1,6 +1,6 @@  - + @@ -52,8 +52,8 @@ ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\businesslogic.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\businesslogic.dll ..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll @@ -61,20 +61,20 @@ ..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\cms.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\cms.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\controls.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\controls.dll ..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll - ..\packages\UmbracoForms.Core.6.0.2\lib\EPPlus.dll + ..\packages\UmbracoForms.Core.6.0.3\lib\EPPlus.dll - - ..\packages\Examine.0.1.83\lib\net45\Examine.dll + + ..\packages\Examine.0.1.88\lib\net45\Examine.dll ..\packages\HtmlAgilityPack.1.4.9.5\lib\Net45\HtmlAgilityPack.dll @@ -82,17 +82,17 @@ ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - ..\packages\ImageProcessor.2.5.3\lib\net45\ImageProcessor.dll + + ..\packages\ImageProcessor.2.5.6\lib\net45\ImageProcessor.dll - - ..\packages\ImageProcessor.Web.4.8.3\lib\net45\ImageProcessor.Web.dll + + ..\packages\ImageProcessor.Web.4.8.7\lib\net45\ImageProcessor.Web.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\interfaces.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\interfaces.dll - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\log4net.dll + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\log4net.dll ..\packages\Log4Net.Async.2.0.4\lib\net40\Log4Net.Async.dll @@ -104,7 +104,7 @@ ..\packages\Markdown.1.14.7\lib\net45\MarkdownSharp.dll - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\Microsoft.ApplicationBlocks.Data.dll + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\Microsoft.ApplicationBlocks.Data.dll ..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll @@ -125,23 +125,23 @@ ..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.1.0.3\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll - - ..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.1\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll + + ..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.2\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll - - ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + + ..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll - - ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + ..\packages\Microsoft.Owin.Host.SystemWeb.3.1.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll + + ..\packages\Microsoft.Owin.Security.3.1.0\lib\net45\Microsoft.Owin.Security.dll - - ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll + + ..\packages\Microsoft.Owin.Security.Cookies.3.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll + + ..\packages\Microsoft.Owin.Security.OAuth.3.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -165,18 +165,18 @@ ..\packages\semver.1.1.2\lib\net451\Semver.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\SQLCE4Umbraco.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\SQLCE4Umbraco.dll ..\packages\System.Collections.Immutable.1.1.36\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll True - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\System.Data.SqlServerCe.dll + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\System.Data.SqlServerCe.dll - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\System.Data.SqlServerCe.Entity.dll + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\System.Data.SqlServerCe.Entity.dll @@ -235,86 +235,84 @@ ..\packages\Terratype.GoogleMapsV3.1.0.13\lib\net45\Terratype.GoogleMapsV3.dll - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\TidyNet.dll + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\TidyNet.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\umbraco.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\umbraco.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\Umbraco.Core.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\Umbraco.Core.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\umbraco.DataLayer.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\umbraco.DataLayer.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\umbraco.editorControls.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\umbraco.editorControls.dll - - ..\packages\UmbracoForms.Core.6.0.2\lib\Umbraco.Forms.Core.dll + + ..\packages\UmbracoForms.Core.6.0.3\lib\Umbraco.Forms.Core.dll - - ..\packages\UmbracoForms.Core.6.0.2\lib\Umbraco.Forms.Core.Providers.dll + + ..\packages\UmbracoForms.Core.6.0.3\lib\Umbraco.Forms.Core.Providers.dll - - ..\packages\UmbracoForms.Core.6.0.2\lib\Umbraco.Forms.Web.dll + + ..\packages\UmbracoForms.Core.6.0.3\lib\Umbraco.Forms.Web.dll - - ..\packages\UmbracoForms.Core.6.0.2\lib\Umbraco.Forms.Web.XmlSerializers.dll + + ..\packages\UmbracoForms.Core.6.0.3\lib\Umbraco.Forms.Web.XmlSerializers.dll - ..\packages\UmbracoForms.Core.6.0.2\lib\Umbraco.Licensing.dll + ..\packages\UmbracoForms.Core.6.0.3\lib\Umbraco.Licensing.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\umbraco.MacroEngines.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\umbraco.MacroEngines.dll ..\packages\Umbraco.ModelsBuilder.3.0.7\lib\Umbraco.ModelsBuilder.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\umbraco.providers.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\umbraco.providers.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\Umbraco.Web.UI.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\Umbraco.Web.UI.dll - - ..\packages\UmbracoCms.Core.7.6.4\lib\net45\UmbracoExamine.dll + + ..\packages\UmbracoCms.Core.7.7.7\lib\net45\UmbracoExamine.dll + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - @@ -327,15 +325,38 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + Web.config @@ -345,26 +366,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -415,11 +416,11 @@ - - + + - + Type Skriv for at søge... Op @@ -498,6 +611,7 @@ Gemmer... nuværende Indlejring + Hent valgt @@ -573,7 +687,6 @@ Normalbrugeren er blevet gjort utjenstdygtig eller har ikke adgang til Umbraco!

Du behøver ikke foretage yderligere handlinger. Tryk på Næste for at fortsætte.

]]>
Normalbrugerens adgangskode er på succesfuld vis blevet ændret siden installationen!

Du behøver ikke foretage yderligere handlinger. Tryk på Næste for at fortsætte.

]]>
Adgangskoden er blevet ændret! - Umbraco opretter en normalbruger med et login ('admin') og en adgangskode ('default').

Det er vigtigt at adgangskoden bliver ændret til noget unikt.

Dette skridt vil kontrollere normalbrugerens adgangskode og foreslå om det er nødt til at blive ændret.

]]>
Få en fremragende start, se vores videoer Ved at klikke på næste knappen (eller ved at ændre UmbracoConfigurationStatus i web.config filen), accepterer du licensaftalen for denne software, som specificeret i boksen nedenfor. Bemærk venligst at denne Umbraco distribution består af to forskellige licenser, MIT's Open Source Licens for frameworket og Umbraco Freeware Licensen som dækker UI'en. Endnu ikke installeret @@ -702,7 +815,40 @@ Mange hilsner fra Umbraco robotten Vælg pakken fra din computer. Umbraco pakker er oftest en ".zip" fil - Udvikler + Slip her for at uploade + eller klik her for at vælge pakkefil + Upload pakke + Installer en lokal pakke ved at vælge den fra din computer. Installer kun pakker fra kilder, du kender og stoler på + Upload en anden pakke + Annuller og upload en anden pakke + Licens + Jeg accepterer + betingelser for anvendelse + Installér pakke + Afslut + Installeret pakker + Du har ingen pakker installeret + 'Pakker' øverst til højre på din skærm]]> + Søg efter pakker + Resultater for + Vi kunne ikke finde resultater for + Prøv venligst at søge efter en anden pakke eller gennemse kategorierne + Populære + Nye udgivelser + har + karma points + Information + Ejer + Bidragsydere + Oprettet + Nuværende version + .NET version + Downloads + Likes + Kompatibilitet + Denne pakke er kompatibel med de følgende versioner af Umbraco, som rapporteret af community-medlemmer. Fuld kompatibilitet kan ikke garanteres for versioner rapporteret nedenfor 100% + Eksterne kilder + Forfatter Demonstration Dokumentation Pakke meta data @@ -726,7 +872,18 @@ Mange hilsner fra Umbraco robotten Opdateringsinstrukser Der er en tilgængelig opdatering til denne pakke. Du kan downloade den direkte fra Umbracos pakke opbevaringsbase. Pakke version + Pakke versionshistorik Se pakkeudviklerens website + Pakke allerede installeret + Denne pakke kan ikke installeres, den kræver en minimum Umbraco version af + Afinstallerer... + Downloader... + Importeret... + Installerer... + Genstarter, vent venligst... + Færdig, din browser opdateres nu, vent venligst... + Klik venligst på 'Afslut' for at gennemføre installation og opdatere siden. + Uploader pakke... Indsæt med fuld formattering (Anbefales ikke) @@ -778,6 +935,15 @@ Mange hilsner fra Umbraco robotten Du har ikke konfigureret nogen godkendte farver + + Du har valgt et dokument som er slettet eller lagt i papirkurven + Du har valgt dokumenter som er slettede eller lagt i papirkurven + + + Du har valgt et medie som er slettet eller lagt i papirkurven + Du har valgt medier som er slettede eller lagt i papirkurven + Slettet medie + indtast eksternt link vælg en intern side @@ -790,108 +956,7 @@ Mange hilsner fra Umbraco robotten Nulstil - - Vælg indholdstype - Vælg layout - Tilføj række - Tilføj indhold - Slip indhold - Indstillinger tilføjet - - Indholdet er ikke tilladt her - Indholdet er tilladt her - - Klik for at indlejre - Klik for at indsætte et billede - Billedtekst... - Skriv her... - - Gridlayout - Et layout er det overordnede arbejdsområde til dit gitter - du vil typisk kun behøve ét eller to - Tilføj gitterlayout - Juster dit layout ved at justere kolonnebredder og tilføj yderligere sektioner - - Rækkekonfigurationer - Rækker er foruddefinerede celler, der arrangeres vandret - Tilføj rækkekonfiguration - Juster rækken ved at indstille cellebredder og tilføje yderligere celler - - Kolonner - Det totale antaller kolonner i gitteret - - Indstillinger - Konfigurer, hvilket indstillinger, brugeren kan ændre - - - Typografi - Vælg, hvilke typografiværdier, en redaktør kan ændre - - Indstillinger gemmes kun, hvis den indtaste json-konfiguration er gyldig - - Tillad alle editorer - Tillad alle rækkekonfigurationer - - Sæt som standard - Vælg ekstra - Vælg standard - er tilføjet - - - - - Kompositioner - Du har ikke tilføjet nogle faner - Tilføj ny fane - Tilføj endnu en fane - Nedarvet fra - Tilføj property - Påkrævet label - - Aktiver listevisning - Konfigurer indholdet til at blive vist i en sorterbar og søgbar liste, dens børn vil ikke blive vist i træet - - Tilladte skabeloner - Vælg hvilke skabeloner der er tilladt at bruge på dette indhold - - Tillad på rodniveau - Kun dokumenttyper med denne indstilling aktiveret oprettes i rodniveau under inhold og mediearkiv - Ja – indhold af denne type er tilladt i roden - - Tilladte typer - Tillad at oprette indhold af en specifik type under denne - - Vælg child node - - Nedarv faner og egenskaber fra en anden dokumenttype. Nye faner vil blive tilføjet den nuværende dokumenttype eller sammenflettet hvis fanenavnene er ens. - Indholdstypen bliver brugt i en komposition og kan derfor ikke blive anvendt som komposition - Der er ingen indholdstyper tilgængelige at bruge som komposition - - Tilgængelige editors - Genbrug - Editor indstillinger - - Konfiguration - - Ja, slet - - blev flyttet til - Vælg hvor - skal flyttes til - - Alle dokumenttyper - Alle dokumenter - Alle medier - - som benytter denne dokumenttype vil blive slettet permanent. Bekræft at du også vil slette dem. - som benytter denne medietype vil blive slettet permanent. Bekræft at du også vil slette dem. - som benytter denne medlemstype vil blive slettet permanent. Bekræft at du også vil slette dem. - - og alle dokumenter, som benytter denne type - og alle medier, som benytter denne type - og alle medlemmer, som benytter denne type - - der bruger denne editor vil blive opdateret med de nye indstillinger - + Nuværende version @@ -949,7 +1014,7 @@ Mange hilsner fra Umbraco robotten Oprettelsesdato Sortering udført Træk de forskellige sider op eller ned for at indstille hvordan de skal arrangeres, eller klik på kolonnehovederne for at sortere hele rækken af sider -
Luk ikke dette vindue imens]]>
+ Annulleret @@ -1007,13 +1072,36 @@ Mange hilsner fra Umbraco robotten Partial view gemt uden fejl! Partial view ikke gemt Der opstod en fejl ved at gemme filen. + Rettigheder gemt for + Script view gemt + Script view gemt uden fejl! + Script view ikke gemt + An error occurred saving the file. + An error occurred saving the file. + Slettede %0% brugergrupper + %0% blev slettet + Aktiverede %0% brugere + Der opstod en fejl under aktivering af brugerne + Deaktiverede %0% brugere + Der opstod en fejl under deaktivering af brugerne + %0% er nu aktiveret + Der opstod en fejl under aktivering af brugeren + %0% er nu deaktiveret + Der opstod en fejl under deaktivering af brugeren + Brugergrupper er blevet indstillet + Slettede %0% brugergrupper + %0% blev slettet + Låste %0% brugere op + Der opstod en fejl under oplåsning af brugerne + %0% er nu låst op + Der opstod en fejl under oplåsning af brugeren - Bruger CSS-syntax f.eks. h1, .redheader, .blueTex + Bruger CSS-syntaks f.eks. h1, .redheader, .blueTex Rediger stylesheet Rediger CSS-egenskab Navn der identificerer CSS-egenskaben i tekstredigeringsværktøjet - Vis prøve + Forhåndsvisning Styles @@ -1126,7 +1214,114 @@ Mange hilsner fra Umbraco robotten Skabelon - + + Vælg indholdstype + Vælg layout + Tilføj række + Tilføj indhold + Slip indhold + Indstillinger tilføjet + + Indholdet er ikke tilladt her + Indholdet er tilladt her + + Klik for at indlejre + Klik for at indsætte et billede + Billedtekst... + Skriv her... + + Gitterlayout + Et layout er det overordnede arbejdsområde til dit gitter - du vil typisk kun behøve ét eller to + Tilføj gitterlayout + Juster dit layout ved at justere kolonnebredder og tilføj yderligere sektioner + + Rækkekonfigurationer + Rækker er foruddefinerede celler, der arrangeres vandret + Tilføj rækkekonfiguration + Juster rækken ved at indstille cellebredder og tilføje yderligere celler + + Kolonner + Det totale antaller kolonner i gitteret + + Indstillinger + Konfigurer, hvilket indstillinger, brugeren kan ændre + + + Typografi + Vælg, hvilke typografiværdier, en redaktør kan ændre + + Indstillinger gemmes kun, hvis den indtaste json-konfiguration er gyldig + + Tillad alle editorer + Tillad alle rækkekonfigurationer + + Sæt som standard + Vælg ekstra + Vælg standard + er tilføjet + + Maksimalt emner + Efterlad blank eller sat til 0 ubegrænset for + + + + + Kompositioner + Du har ikke tilføjet nogle faner + Tilføj ny fane + Tilføj endnu en fane + Nedarvet fra + Tilføj property + Påkrævet label + + Aktiver listevisning + Konfigurer indholdet til at blive vist i en sorterbar og søgbar liste, dens børn vil ikke blive vist i træet + + Tilladte skabeloner + Vælg hvilke skabeloner der er tilladt at bruge på dette indhold + + Tillad på rodniveau + Kun dokumenttyper med denne indstilling aktiveret oprettes i rodniveau under inhold og mediearkiv + Ja – indhold af denne type er tilladt i roden + + Tilladte typer + Tillad at oprette indhold af en specifik type under denne + + Vælg child node + + Nedarv faner og egenskaber fra en anden dokumenttype. Nye faner vil blive tilføjet den nuværende dokumenttype eller sammenflettet hvis fanenavnene er ens. + Indholdstypen bliver brugt i en komposition og kan derfor ikke blive anvendt som komposition + Der er ingen indholdstyper tilgængelige at bruge som komposition + + Tilgængelige editors + Genbrug + Editor indstillinger + + Konfiguration + + Ja, slet + + blev flyttet til + Vælg hvor + skal flyttes til + + Alle dokumenttyper + Alle dokumenter + Alle medier + + som benytter denne dokumenttype vil blive slettet permanent. Bekræft at du også vil slette dem. + som benytter denne medietype vil blive slettet permanent. Bekræft at du også vil slette dem. + som benytter denne medlemstype vil blive slettet permanent. Bekræft at du også vil slette dem. + + og alle dokumenter, som benytter denne type + og alle medier, som benytter denne type + og alle medlemmer, som benytter denne type + + der bruger denne editor vil blive opdateret med de nye indstillinger + Medlem kan redigere + Vis på medlemsprofil + fane har ingen sorteringsrækkefølge + Alternativt felt Alternativ tekst @@ -1145,8 +1340,6 @@ Mange hilsner fra Umbraco robotten Indsæt efter felt Indsæt før felt Rekursivt - Fjern paragraf-tags - Fjerner eventuelle <P> omkring teksten Standard felter Uppercase URL encode @@ -1184,6 +1377,9 @@ Mange hilsner fra Umbraco robotten Upload oversættelse (xml) + Indhold + Indholdsskabeloner + Mediearkiv Cacheviser Papirkurv Oprettede pakker @@ -1202,9 +1398,10 @@ Mange hilsner fra Umbraco robotten Medlemstype Dokumenttyper Relationstyper - Pakker Pakker + Partial Views + Partial View Makro Filer Python Installer fra "repository" Installer Runway @@ -1215,6 +1412,7 @@ Mange hilsner fra Umbraco robotten Skabeloner XSLT-filer Analytics + Brugere Ny opdatering er klar @@ -1223,25 +1421,48 @@ Mange hilsner fra Umbraco robotten Der kunne ikke tjekkes for ny opdatering. Se trace for mere info. + Adgang + Baseret på de tildelte grupper og startnoder har brugeren adgang til følgende noder + Tildel adgang Administrator Kategorifelt + Bruger oprettet Skift dit kodeord + Skift billede Nyt kodeord + er ikke blevet låst ude + Kodeordet er ikke blevet ændret Gentag dit nye kodeord Du kan ændre dit kodeord, som giver dig adgang til Umbraco Back Office ved at udfylde formularen og klikke på knappen 'Skift dit kodeord' Indholdskanal + Skift billede + Opret nye brugere for at give dem adgang til Umbraco. Når en ny bruger oprettes, genereres der en adgangskode, som du kan dele med brugeren. Beskrivelsesfelt Deaktivér bruger Dokumenttype Redaktør Uddragsfelt + Fejlet loginforsøg + Gå til brugerprofil + Tilføj grupper for at tildele adgang og tilladelser + Invitér anden bruger + Invitér nye brugere til at give dem adgang til Umbraco. En invitation vil blive sendt via e-mail til brugeren med oplysninger om, hvordan man logger ind i Umbraco. Sprog + Indstil det sprog, du vil se i menuer og dialoger + Seneste låst ude dato + Senest login + Kodeord sidst ændret Brugernavn Startnode i mediearkivet + Begræns mediebiblioteket til en bestemt startnode + Medie startnoder + Begræns mediebiblioteket til bestemte startnoder Moduler Deaktivér adgang til Umbraco + har endnu ikke logget ind Gammelt kodeord Adgangskode + Fjern billede Nulstil kodeord Dit kodeord er blevet ændret! Bekræft venligst dit nye kodeord @@ -1254,26 +1475,74 @@ Mange hilsner fra Umbraco robotten Erstat underelement-rettigheder Du ændrer i øjeblikket rettigheder for siderne: Vælg sider for at ændre deres rettigheder + Fjern billede + Standard rettigheder + Granulære rettigheder + Sæt rettigheder for specifikke noder + Profil Søg alle 'børn' Start node + Aktiv + Alle + Deaktiveret + Låst ude + Inviteret + Navn (A-Å) + Navn (Å-A) + Nyeste + Ældste + Senest login + Tilføj sektioner for at give brugerne adgang + Vælg brugergrupper + Ingen startnode valgt + Ingen startnoder valgt + Startnode + Begræns indholdstræet til en bestemt startnode + Indhold startnoder + Begræns indholdstræet til bestemte startnoder + Bruger sidst opdateret + er blevet oprettet + Den nye bruger er blevet oprettet. For at logge ind i Umbraco skal du bruge adgangskoden nedenfor. + Brugeradministration Navn Brugertilladelser - Brugertype - Brugertyper + Brugergruppe tilladelser + Brugergruppe + Brugergrupper + er blevet inviteret + En invitation er blevet sendt til den nye bruger med oplysninger om, hvordan man logger ind i Umbraco. + Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode og tilføje et billede til din avatar. + Upload et billede for at gøre det nemt for andre brugere at genkende dig. Forfatter Oversætter Skift Din profil Din historik Session udløber + Invitér bruger + Opret bruger + Send invitation + Tilbage til brugere + Umbraco: Invitation + +

Hej %0%, du er blevet inviteret af %1% til Umbraco backoffice.

Besked fra %1%: %2%

Klik på dette link for acceptere invitationen

Hvis du ikke kan klikke på linket, så kopier og indsæt denne URL i dit browservindue

%3%

]]> +
Validation - Valider som email + Valider som e-mail Valider som tal Valider som Url ...eller indtast din egen validering Feltet er påkrævet + Indtast et regulært udtryk + Du skal tilføje mindst + Du kan kun have + elementer + elementer valgt + Ugyldig dato + Ikke et tal + Ugyldig e-mail Slå URL tracker fra @@ -1292,4 +1561,10 @@ Mange hilsner fra Umbraco robotten URL tracker er nu slået fra. Der opstod en fejl under forsøget på at slå URL trackeren til, der findes mere information i logfilen. + + Ingen ordbog elementer at vælge imellem + + + Karakterer tilbage + diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/de.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/de.xml index e75e0cbe..1ad298d2 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/de.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/de.xml @@ -33,10 +33,8 @@ Zur Veröffentlichung einreichen Zur Übersetzung senden Sortieren - Zur Veröffentlichung einreichen Übersetzen Aktualisieren - Standardwert Erlaubnis verweigert. @@ -221,6 +219,8 @@ Copied %0% out of %1% items + Name des Link + Link Name Hostnamen verwalten Fenster schließen @@ -480,13 +480,6 @@ <strong>Der Standard-Benutzer wurde deaktiviert oder hat keinen Zugriff auf Umbraco.</strong></p><p>Es sind keine weiteren Aktionen notwendig. Klicken Sie auf <b>Weiter</b> um fortzufahren. <strong>Das Kennwort des Standard-Benutzers wurde seit der Installation verändert.</strong></p><p>Es sind keine weiteren Aktionen notwendig. Klicken Sie auf <b>Weiter</b> um fortzufahren. Das Kennwort wurde geändert! - <p> - Bei der Installation von Umbraco wurde ein Standard-Benutzer mit dem Login-Namen <strong>'admin'</strong> und dem Kennwort <strong>'default'</strong> erstellt. - <strong>WICHTIG:</strong> Das Kennwort sollte auf ein sicheres, eigenes Kennwort geändert werden. - </p> - <p> - Das Kennwort des Standard-Benutzers wird jetzt geprüft und im Anschluss werden eventuell notwendige Änderungen vorschlagen. - </p> Schauen Sie sich die Einführungsvideos für einen schnellen und einfachen Start an. Mit der Installation stimmen Sie der angezeigten Lizenz für diese Software zu. Bitte beachten Sie, dass diese Umbraco-Distribution aus zwei Lizenzen besteht. Einer freien Open-Source MIT-Lizenz für das Framework und der Umbraco-Freeware-Lizenz für die Verwaltungsoberfläche. Noch nicht installiert. @@ -771,7 +764,7 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Erstellungsdatum Sortierung abgeschlossen. Ziehen Sie die Elemente an ihre gewünschte neue Position. - Bitte warten, die Seiten werden sortiert. Das kann einen Moment dauern. Bitte schließen Sie dieses Fenster nicht, bis der Sortiervorgang abgeschlossen ist. + Bitte warten, die Seiten werden sortiert. Das kann einen Moment dauern. Fehlgeschlagen @@ -859,7 +852,7 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Element hinzufügen Zeilenlayout auswählen - Einfach auf <i class="icon icon-add blue"></i> klicken, um das erste Element anzulegen + Element mit <i class="icon icon-add blue"></i> hinzufügen Drop content Klicken, um Inhalt einzubetten Klicken, um Abbildung einzufügen @@ -903,8 +896,6 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die An den Feldinhalt anhängen Dem Feldinhalt voranstellen Rekursiv - Textabsatz entfernen - Alle <p> am Anfang und am Ende des Feldinhalts werden entfernt Standardfelder Großbuchstaben URL kodieren @@ -1024,8 +1015,6 @@ Ihr freundlicher Umbraco-Robot Startelement in den Inhalten Benutzername Berechtigungen - Rolle - Rollen Autor Übersetzer Ihr Profil diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/en.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/en.xml index ebe724f7..11968679 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/en.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/en.xml @@ -1,673 +1,791 @@ - + - - The Umbraco community - http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files - - - Culture and Hostnames - Audit Trail - Browse Node - Change Document Type - Copy - Create - Create Package - Delete - Disable - Empty recycle bin - Export Document Type - Import Document Type - Import Package - Edit in Canvas - Exit - Move - Notifications - Public access - Publish - Unpublish - Reload - Republish entire site - Restore - Set permissions for the page %0% - Choose where to move - to in the tree structure below - Permissions - Rollback - Send To Publish - Send To Translation - Sort - Send to publication - Translate - Update - Default value - Export formImport formArchive formUnarchive formDefault value - - Permission denied. - Add new Domain - remove - Invalid node. - Invalid domain format. - Domain has already been assigned. - Language - Domain - New domain '%0%' has been created - Domain '%0%' is deleted - Domain '%0%' has already been assigned - Domain '%0%' has been updated - Edit Current Domains - + The Umbraco community + http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files + + + Culture and Hostnames + Audit Trail + Browse Node + Change Document Type + Copy + Create + Create Package + Create group + Delete + Disable + Empty recycle bin + Enable + Export Document Type + Import Document Type + Import Package + Edit in Canvas + Exit + Move + Notifications + Public access + Publish + Unpublish + Reload + Republish entire site + Rename + Restore + Set permissions for the page %0% + Choose where to move + In the tree structure below + Permissions + Rollback + Send To Publish + Send To Translation + Set group + Sort + Translate + Update + Set permissions + Unlock + Create Content Template + + + Content + Administration + Structure + Other + + + Allow access to assign culture and hostnames + Allow access to view a node's history log + Allow access to view a node + Allow access to change document type for a node + Allow access to copy a node + Allow access to create nodes + Allow access to delete nodes + Allow access to move a node + Allow access to set and change public access for a node + Allow access to publish a node + Allow access to change permissions for a node + Allow access to roll back a node to a previous state + Allow access to send a node for approval before publishing + Allow access to send a node for translation + Allow access to change the sort order for nodes + Allow access to translate a node + Allow access to save a node + Allow access to create a Content Template + + + Permission denied. + Add new Domain + remove + Invalid node. + Invalid domain format. + Domain has already been assigned. + Language + Domain + New domain '%0%' has been created + Domain '%0%' is deleted + Domain '%0%' has already been assigned + Domain '%0%' has been updated + Edit Current Domains + + - Inherit - Culture - or inherit culture from parent nodes. Will also apply
- to the current node, unless a domain below applies too.]]>
- Domains - - - Viewing for - - - Clear selection - Select - Select current folder - Do something else - Bold - Cancel Paragraph Indent - Insert form field - Insert graphic headline - Edit Html - Indent Paragraph - Italic - Center - Justify Left - Justify Right - Insert Link - Insert local link (anchor) - Bullet List - Numeric List - Insert macro - Insert picture - Edit relations - Return to list - Save - Save and publish - Save and send for approval - Save list view - Preview - Preview is disabled because there's no template assigned - Choose style - Show styles - Insert table - Generate models - Undo - Redo - - - To change the document type for the selected content, first select from the list of valid types for this location. - Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. - The content has been re-published. - Current Property - Current type - The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. - Document Type Changed - Map Properties - Map to Property - New Template - New Type - none - Content - Select New Document Type - The document type of the selected content has been successfully changed to [new type] and the following properties mapped: - to - Could not complete property mapping as one or more properties have more than one mapping defined. - Only alternate types valid for the current location are displayed. - - - Is Published - About this page - Alias - (how would you describe the picture over the phone) - Alternative Links - Click to edit this item - Created by - Original author - Updated by - Created - Date/time this document was created - Document Type - Editing - Remove at - This item has been changed after publication - This item is not published - Last published - There are no items to show - There are no items to show in the list. - Media Type - Link to media item(s) - Member Group - Role - Member Type - No date chosen - Link title - Properties - This document is published but is not visible because the parent '%0%' is unpublished - This document is published but is not in the cache - Could not get the url - This document is published but its url would collide with content %0% - Publish - Publication Status - Publish at - Unpublish at - Clear Date - Sortorder is updated - To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting - Statistics - Title (optional) - Alternative text (optional) - Type - Unpublish - Last edited - Date/time this document was edited - Remove file(s) - Link to document - Member of group(s) - Not a member of group(s) - Child items - Target - This translates to the following time on the server: - What does this mean?]]> - - - Click to upload - Drop your files here... - Link to media - or click here to choose files - Only allowed file types are - Max file size is - - - Create a new member - All Members - - - Where do you want to create the new %0% - Create an item under - Choose a type and a title - "document types".]]> - "media types".]]> - Document Type without a template - New folder - New data type - New javascript file - New empty partial view - New partial view macro - New partial view from snippet - New empty partial view macro - New partial view macro from snippet - New partial view macro (without macro) - - - Browse your website - - Hide - If Umbraco isn't opening, you might need to allow popups from this site - has opened in a new window - Restart - Visit - Welcome - - - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes - - - Done - - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items - - - Name - Manage hostnames - Close this window - Are you sure you want to delete - Are you sure you want to disable - Please check this box to confirm deletion of %0% item(s) - Are you sure? - Are you sure? - Cut - Edit Dictionary Item - Edit Language - Insert local link - Insert character - Insert graphic headline - Insert picture - Insert link - Click to add a Macro - Insert table - Last Edited - Link - Internal link: - When using local links, insert "#" in front of link - Open in new window? - Macro Settings - This macro does not contain any properties you can edit - Paste - Edit Permissions for - The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place - The recycle bin is now empty - When items are deleted from the recycle bin, they will be gone forever - regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> - Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' - Remove Macro - Required Field - Site is reindexed - The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished - The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. - Number of columns - Number of rows - Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, - by referring this ID using a <asp:content /> element.]]> - Select a placeholder id from the list below. You can only - choose Id's from the current template's master.]]> - Click on the image to see full size - Pick item - View Cache Item - Create folder... - Relate to original - Include descendants - The friendliest community - Link to page - Opens the linked document in a new window or tab - Link to media - Select media - Select icon - Select item - Select link - Select macro - Select content - Select member - Select member group - No icons were found - There are no parameters for this macro - There are no macros available to insert - External login providers - Exception Details - Stacktrace - Inner Exception - Link your - Un-Link your - account - Select editor - - - + + Inherit + Culture + + or inherit culture from parent nodes. Will also apply
+ to the current node, unless a domain below applies too.]]> +
+ Domains + + + Viewing for + + + Clear selection + Select + Select current folder + Do something else + Bold + Cancel Paragraph Indent + Insert form field + Insert graphic headline + Edit Html + Indent Paragraph + Italic + Center + Justify Left + Justify Right + Insert Link + Insert local link (anchor) + Bullet List + Numeric List + Insert macro + Insert picture + Edit relations + Return to list + Save + Save and publish + Save and send for approval + Save list view + Preview + Preview is disabled because there's no template assigned + Choose style + Show styles + Insert table + Generate models + Save and generate models + Undo + Redo + + + To change the document type for the selected content, first select from the list of valid types for this location. + Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. + The content has been re-published. + Current Property + Current type + The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. + Document Type Changed + Map Properties + Map to Property + New Template + New Type + none + Content + Select New Document Type + The document type of the selected content has been successfully changed to [new type] and the following properties mapped: + to + Could not complete property mapping as one or more properties have more than one mapping defined. + Only alternate types valid for the current location are displayed. + + + Is Published + About this page + Alias + (how would you describe the picture over the phone) + Alternative Links + Click to edit this item + Created by + Original author + Updated by + Created + Date/time this document was created + Document Type + Editing + Remove at + This item has been changed after publication + This item is not published + Last published + There are no items to show + There are no items to show in the list. + No content has been added + No members have been added + Media Type + Link to media item(s) + Member Group + Role + Member Type + No date chosen + Page title + Properties + This document is published but is not visible because the parent '%0%' is unpublished + This document is published but is not in the cache + Could not get the url + This document is published but its url would collide with content %0% + Publish + Publication Status + Publish at + Unpublish at + Clear Date + Sortorder is updated + To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting + Statistics + Title (optional) + Alternative text (optional) + Type + Unpublish + Last edited + Date/time this document was edited + Remove file(s) + Link to document + Member of group(s) + Not a member of group(s) + Child items + Target + This translates to the following time on the server: + What does this mean?]]> + Are you sure you want to delete this item? + Property %0% uses editor %1% which is not supported by Nested Content. + Add another text box + Remove this text box + Content root + + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is pre-defined content that an editor can select to use as the basis for creating new content + + + Click to upload + Drop your files here... + Link to media + or click here to choose files + Only allowed file types are + Cannot upload this file, it does not have an approved file type + Max file size is + Media root + + + Create a new member + All Members + + + Where do you want to create the new %0% + Create an item under + Select the document type you want to make a content template for + Choose a type and a title + "document types".]]> + "media types".]]> + Document Type without a template + New folder + New data type + New javascript file + New empty partial view + New partial view macro + New partial view from snippet + New empty partial view macro + New partial view macro from snippet + New partial view macro (without macro) + + + Browse your website + - Hide + If Umbraco isn't opening, you might need to allow popups from this site + has opened in a new window + Restart + Visit + Welcome + + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + + + Done + + Deleted %0% item + Deleted %0% items + Deleted %0% out of %1% item + Deleted %0% out of %1% items + + Published %0% item + Published %0% items + Published %0% out of %1% item + Published %0% out of %1% items + + Unpublished %0% item + Unpublished %0% items + Unpublished %0% out of %1% item + Unpublished %0% out of %1% items + + Moved %0% item + Moved %0% items + Moved %0% out of %1% item + Moved %0% out of %1% items + + Copied %0% item + Copied %0% items + Copied %0% out of %1% item + Copied %0% out of %1% items + + + Link title + Link + Name + Manage hostnames + Close this window + Are you sure you want to delete + Are you sure you want to disable + Please check this box to confirm deletion of %0% item(s) + Are you sure? + Are you sure? + Cut + Edit Dictionary Item + Edit Language + Insert local link + Insert character + Insert graphic headline + Insert picture + Insert link + Click to add a Macro + Insert table + Last Edited + Link + Internal link: + When using local links, insert "#" in front of link + Open in new window? + Macro Settings + This macro does not contain any properties you can edit + Paste + Edit permissions for + Set permissions for + Set permissions for %0% for user group %1% + Select the users groups you want to set permissions for + The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place + The recycle bin is now empty + When items are deleted from the recycle bin, they will be gone forever + regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> + Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' + Remove Macro + Required Field + Site is reindexed + The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished + The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. + Number of columns + Number of rows + + Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, + by referring this ID using a <asp:content /> element.]]> + + + Select a placeholder id from the list below. You can only + choose Id's from the current template's master.]]> + + Click on the image to see full size + Pick item + View Cache Item + Create folder... + Relate to original + Include descendants + The friendliest community + Link to page + Opens the linked document in a new window or tab + Link to media + Link to file + Select content start node + Select media + Select icon + Select item + Select link + Select macro + Select content + Select media start node + Select member + Select member group + Select node + Select sections + Select users + No icons were found + There are no parameters for this macro + There are no macros available to insert + External login providers + Exception Details + Stacktrace + Inner Exception + Link your + Un-link your + account + Select editor + Select snippet + + + + %0%' below
You can add additional languages under the 'languages' in the menu on the left - ]]>
- Culture Name - Edit the key of the dictionary item. - - + + Culture Name + Edit the key of the dictionary item. + + - - - - Enter your username - Enter your password - Confirm your password - Name the %0%... - Enter a name... - Label... - Enter a description... - Type to search... - Type to filter... - Type to add tags (press enter after each tag)... - Enter your email - Your username is usually your email - - - Allow at root - Only Content Types with this checked can be created at the root level of Content and Media trees - Allowed child node types - Document Type Compositions - Create - Delete tab - Description - New tab - Tab - Thumbnail - Enable list view - Configures the content item to show a sortable & searchable list of its children, the children will not be shown in the tree - Current list view - The active list view data type - Create custom list view - Remove custom list view - - - Add prevalue - Database datatype - Property editor GUID - Property editor - Buttons - Enable advanced settings for - Enable context menu - Maximum default size of inserted images - Related stylesheets - Show label - Width and height - - - Your data has been saved, but before you can publish this page there are some errors you need to fix first: - The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) - %0% already exists - There were errors: - There were errors: - The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) - %0% must be an integer - The %0% field in the %1% tab is mandatory - %0% is a mandatory field - %0% at %1% is not in a correct format - %0% is not in a correct format - - - Received an error from the server - The specified file type has been disallowed by the administrator - NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. - Please fill both alias and name on the new property type! - There is a problem with read/write access to a specific file or folder - Error loading Partial View script (file: %0%) - Error loading userControl '%0%' - Error loading customControl (Assembly: %0%, Type: '%1%') - Error loading MacroEngine script (file: %0%) - "Error parsing XSLT file: %0% - "Error reading XSLT file: %0% - Please enter a title - Please choose a type - You're about to make the picture larger than the original size. Are you sure that you want to proceed? - Error in python script - The python script has not been saved, because it contained error(s) - Startnode deleted, please contact your administrator - Please mark content before changing style - No active styles available - Please place cursor at the left of the two cells you wish to merge - You cannot split a cell that hasn't been merged. - Error in XSLT source - The XSLT has not been saved, because it contained error(s) - There is a configuration error with the data type used for this property, please check the data type - - - About - Action - Actions - Add - Alias - All - Are you sure? - Back - Border - by - Cancel - Cell margin - Choose - Close - Close Window - Comment - Confirm - Constrain proportions - Continue - Copy - Create - Database - Date - Default - Delete - Deleted - Deleting... - Design - Dimensions - Down - Download - Edit - Edited - Elements - Email - Error - Find - Height - Help - Icon - Import - Inner margin - Insert - Install - Invalid - Justify - Language - Layout - Loading - Locked - Login - Log off - Logout - Macro - Mandatory - Move - More - Name - New - Next - No - of - OK - Open - or - Password - Path - Placeholder ID - One moment please... - Previous - Properties - Email to receive form data - Recycle Bin - Remaining - Rename - Renew - Required - Retry - Permissions - Search - Sorry, we can not find what you are looking for - No items have been added - Server - Show - Show page on Send - Size - Sort - Submit - Type - Type to search... - Up - Update - Upgrade - Upload - Url - User - Username - Value - View - Welcome... - Width - Yes - Folder - Search results - Reorder - I am done reordering - Preview - Change password - to - List view - Saving... - current - Embed - selected - - - - Black - Green - Yellow - Orange - Blue - Red - - - - Add tab - Add property - Add editor - Add template - Add child node - Add child - - Edit data type - - Navigate sections - - Shortcuts - show shortcuts - - Toggle list view - Toggle allow as root - - Comment/Uncomment lines - Remove line - Copy Lines Up - Copy Lines Down - Move Lines Up - Move Lines Down - - General - Editor - - - - Background colour - Bold - Text colour - Font - Text - - - - Page - - - The installer cannot connect to the database. - Could not save the web.config file. Please modify the connection string manually. - Your database has been found and is identified as - Database configuration - + + + Enter your username + Enter your password + Confirm your password + Name the %0%... + Enter a name... + Enter an email... + Enter a username... + Label... + Enter a description... + Type to search... + Type to filter... + Type to add tags (press enter after each tag)... + Enter your email... + Enter a message... + Your username is usually your email + + + Allow at root + Only Content Types with this checked can be created at the root level of Content and Media trees + Allowed child node types + Document Type Compositions + Create + Delete tab + Description + New tab + Tab + Thumbnail + Enable list view + Configures the content item to show a sortable & searchable list of its children, the children will not be shown in the tree + Current list view + The active list view data type + Create custom list view + Remove custom list view + + + Renamed + Enter a new folder name here + %0% was renamed to %1% + + + Add prevalue + Database datatype + Property editor GUID + Property editor + Buttons + Enable advanced settings for + Enable context menu + Maximum default size of inserted images + Related stylesheets + Show label + Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath + + + Your data has been saved, but before you can publish this page there are some errors you need to fix first: + The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) + %0% already exists + There were errors: + There were errors: + The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) + %0% must be an integer + The %0% field in the %1% tab is mandatory + %0% is a mandatory field + %0% at %1% is not in a correct format + %0% is not in a correct format + + + Received an error from the server + The specified file type has been disallowed by the administrator + NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. + Please fill both alias and name on the new property type! + There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Error loading userControl '%0%' + Error loading customControl (Assembly: %0%, Type: '%1%') + Error loading MacroEngine script (file: %0%) + "Error parsing XSLT file: %0% + "Error reading XSLT file: %0% + Please enter a title + Please choose a type + You're about to make the picture larger than the original size. Are you sure that you want to proceed? + Error in python script + The python script has not been saved, because it contained error(s) + Startnode deleted, please contact your administrator + Please mark content before changing style + No active styles available + Please place cursor at the left of the two cells you wish to merge + You cannot split a cell that hasn't been merged. + Error in XSLT source + The XSLT has not been saved, because it contained error(s) + There is a configuration error with the data type used for this property, please check the data type + + + About + Action + Actions + Add + Alias + All + Are you sure? + Back + Border + by + Cancel + Cell margin + Choose + Close + Close Window + Comment + Confirm + Constrain + Constrain proportions + Continue + Copy + Create + Database + Date + Default + Delete + Deleted + Deleting... + Design + Dictionary + Dimensions + Down + Download + Edit + Edited + Elements + Email + Error + Find + First + Groups + Height + Help + Hide + Icon + Import + Inner margin + Insert + Install + Invalid + Justify + Label + Language + Last + Layout + Loading + Locked + Login + Log off + Logout + Macro + Mandatory + Message + Move + More + Name + New + Next + No + of + Off + OK + Open + On + or + Order by + Password + Path + Placeholder ID + One moment please... + Previous + Properties + Email to receive form data + Recycle Bin + Your recycle bin is empty + Remaining + Remove + Rename + Renew + Required + Retrieve + Retry + Permissions + Search + Sorry, we can not find what you are looking for + No items have been added + Server + Show + Show page on Send + Size + Sort + Status + Submit + Type + Type to search... + Up + Update + Upgrade + Upload + Url + User + Username + Value + View + Welcome... + Width + Yes + Folder + Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + Retrieve + selected + + + + Black + Green + Yellow + Orange + Blue + Red + + + + Add tab + Add property + Add editor + Add template + Add child node + Add child + + Edit data type + + Navigate sections + + Shortcuts + show shortcuts + + Toggle list view + Toggle allow as root + + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor + + + Background colour + Bold + Text colour + Font + Text + + + Page + + + The installer cannot connect to the database. + Could not save the web.config file. Please modify the connection string manually. + Your database has been found and is identified as + Database configuration + + install button to install the Umbraco %0% database - ]]> - Next to proceed.]]> - Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

+ ]]> +
+ Next to proceed.]]> + + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

Click the retry button when done.
- More information on editing web.config here.

]]>
- + More information on editing web.config here.

]]> +
+ + Please contact your ISP if necessary. - If you're installing on a local machine or server you might need information from your system administrator.]]> - + + + Press the upgrade button to upgrade your database to Umbraco %0%

Don't worry - no content will be deleted and everything will continue working afterwards!

- ]]>
- Press Next to - proceed. ]]> - next to continue the configuration wizard]]> - The Default users' password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> - The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> - The password is changed! - - Umbraco creates a default user with a login ('admin') and password ('default'). It's important that the password is - changed to something unique. -

-

- This step will check the default user's password and suggest if it needs to be changed. -

- ]]>
- Get a great start, watch our introduction videos - By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. - Not installed yet. - Affected files and folders - More information on setting up permissions for Umbraco here - You need to grant ASP.NET modify permissions to the following files/folders - Your permission settings are almost perfect!

- You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
- How to Resolve - Click here to read the text version - video tutorial on setting up folder permissions for Umbraco or read the text version.]]> - Your permission settings might be an issue! + ]]> + + + Press Next to + proceed. ]]> + + next to continue the configuration wizard]]> + The Default users' password needs to be changed!]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> + The password is changed! + Get a great start, watch our introduction videos + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. + Not installed yet. + Affected files and folders + More information on setting up permissions for Umbraco here + You need to grant ASP.NET modify permissions to the following files/folders + + Your permission settings are almost perfect!

+ You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]> +
+ How to Resolve + Click here to read the text version + video tutorial on setting up folder permissions for Umbraco or read the text version.]]> + + Your permission settings might be an issue!

- You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
- Your permission settings are not ready for Umbraco! + You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]> + + + Your permission settings are not ready for Umbraco!

- In order to run Umbraco, you'll need to update your permission settings.]]>
- Your permission settings are perfect!

- You are ready to run Umbraco and install packages!]]>
- Resolving folder issue - Follow this link for more information on problems with ASP.NET and creating folders - Setting up folder permissions - + + + Your permission settings are perfect!

+ You are ready to run Umbraco and install packages!]]> +
+ Resolving folder issue + Follow this link for more information on problems with ASP.NET and creating folders + Setting up folder permissions + + - I want to start from scratch - + + I want to start from scratch + + learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> - You've just set up a clean Umbraco platform. What do you want to do next? - Runway is installed - + + You've just set up a clean Umbraco platform. What do you want to do next? + Runway is installed + + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> - Only recommended for experienced users - I want to start with a simple website - + + Only recommended for experienced users + I want to start with a simple website + + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, @@ -678,87 +796,181 @@ Included with Runway: Home page, Getting Started page, Installing Modules page.
Optional Modules: Top Navigation, Sitemap, Contact, Gallery. - ]]>
- What is Runway - Step 1/5 Accept license - Step 2/5: Database configuration - Step 3/5: Validating File Permissions - Step 4/5: Check Umbraco security - Step 5/5: Umbraco is ready to get you started - Thank you for choosing Umbraco - Browse your new site -You installed Runway, so why not see how your new website looks.]]> - Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> - Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, -you can find plenty of resources on our getting started pages.]]>
- Launch Umbraco -To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> - Connection to database failed. - Umbraco Version 3 - Umbraco Version 4 - Watch - Umbraco %0% for a fresh install or upgrading from version 3.0. + ]]> + + What is Runway + Step 1/5 Accept license + Step 2/5: Database configuration + Step 3/5: Validating File Permissions + Step 4/5: Check Umbraco security + Step 5/5: Umbraco is ready to get you started + Thank you for choosing Umbraco + + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + + + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + + Umbraco %0% is installed and ready for use + + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + + + started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]> +
+ + Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + + Connection to database failed. + Umbraco Version 3 + Umbraco Version 4 + Watch + + Umbraco %0% for a fresh install or upgrading from version 3.0.

- Press "next" to start the wizard.]]>
- - - Culture Code - Culture Name - - - You've been idle and logout will automatically occur in - Renew now to save your work - - - Happy super Sunday - Happy manic Monday - Happy tubular Tuesday - Happy wonderful Wednesday - Happy thunderous Thursday - Happy funky Friday - Happy Caturday - Log in below - Sign in with - Session timed out - © 2001 - %0%
Umbraco.com

]]>
- Forgotten password? - An email will be sent to the address specified with a link to reset your password - An email with password reset instructions will be sent to the specified address if it matched our records - Return to login form - Please provide a new password - Your Password has been updated - The link you have clicked on is invalid or has expired - Umbraco: Reset Password - - Your username to login to the Umbraco back-office is: %0%

Click here to reset your password or copy/paste this URL into your browser:

%1%

]]> -
- - - Dashboard - Sections - Content - - - Choose page above... - %0% has been copied to %1% - Select where the document %0% should be copied to below - %0% has been moved to %1% - Select where the document %0% should be moved to below - has been selected as the root of your new content, click 'ok' below. - No node selected yet, please select a node in the list above before clicking 'ok' - The current node is not allowed under the chosen node because of its type - The current node cannot be moved to one of its subpages - The current node cannot exist at the root - The action isn't allowed since you have insufficient permissions on 1 or more child documents. - Relate copied items to original - - - Edit your notification for %0% - "next" to start the wizard.]]> + + + + Culture Code + Culture Name + + + You've been idle and logout will automatically occur in + Renew now to save your work + + + Happy super Sunday + Happy manic Monday + Happy tubular Tuesday + Happy wonderful Wednesday + Happy thunderous Thursday + Happy funky Friday + Happy Caturday + Log in below + Sign in with + Session timed out + © 2001 - %0%
Umbraco.com

]]>
+ Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Password reset requested +

+

+ Your username to login to the Umbraco back-office is: %0% +

+

+ + + + + + +
+ + Click this link to reset your password + +
+

+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %1% + +
+

+
+
+


+
+
+ + + ]]> +
+ + + Dashboard + Sections + Content + + + Choose page above... + %0% has been copied to %1% + Select where the document %0% should be copied to below + %0% has been moved to %1% + Select where the document %0% should be moved to below + has been selected as the root of your new content, click 'ok' below. + No node selected yet, please select a node in the list above before clicking 'ok' + The current node is not allowed under the chosen node because of its type + The current node cannot be moved to one of its subpages + The current node cannot exist at the root + The action isn't allowed since you have insufficient permissions on 1 or more child documents. + Relate copied items to original + + + Edit your notification for %0% + + - Hi %0%

- -

This is an automated mail to inform you that the task '%1%' - has been performed on the page '%2%' - by the user '%3%' -

- -

-

Update summary:

- - %6% + ]]> + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ +
+
+ + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' +

+ + + + + + +
+ +
+ EDIT
+
+

+

Update summary:

+ + %6% +
+

+

+ Have a nice day!

+ Cheers from the Umbraco robot +

+
+
+


+
+
-

- - - -

Have a nice day!

- Cheers from the Umbraco robot -

]]>
- [%0%] Notification about %1% performed on %2% - Notifications - - - + + ]]> + + [%0%] Notification about %1% performed on %2% + Notifications + + + + - button and locating the package. Umbraco packages usually have a ".zip" extension. - ]]> - Author - Demonstration - Documentation - Package meta data - Package name - Package doesn't contain any items -
- You can safely remove this from the system by clicking "uninstall package" below.]]>
- No upgrades available - Package options - Package readme - Package repository - Confirm uninstall - Package was uninstalled - The package was successfully uninstalled - Uninstall package - + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. + ]]> + + Drop to upload + or click here to choose package file + Upload package + Install a local package by selecting it from your machine. Only install packages from sources you know and trust + Upload another package + Cancel and upload another package + License + I accept + terms of use + Install package + Finish + Installed packages + You don’t have any packages installed + 'Packages' icon in the top right of your screen]]> + Search for packages + Results for + We couldn’t find anything for + Please try searching for another package or browse through the categories + Popular + New releases + has + karma points + Information + Owner + Contributors + Created + Current version + .NET version + Downloads + Likes + Compatibility + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100% + External sources + Author + Demonstration + Documentation + Package meta data + Package name + Package doesn't contain any items + +
+ You can safely remove this from the system by clicking "uninstall package" below.]]> +
+ No upgrades available + Package options + Package readme + Package repository + Confirm package uninstall + Package was uninstalled + The package was successfully uninstalled + Uninstall package + + Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, - so uninstall with caution. If in doubt, contact the package author.]]> - Download update from the repository - Upgrade package - Upgrade instructions - There's an upgrade available for this package. You can download it directly from the Umbraco package repository. - Package version - Package version history - View package website - Package already installed - This package cannot be installed, it requires a minimum Umbraco version of %0% - Uninstalling... - Downloading... - Importing... - Installing... - Restarting, please wait... - All done, your browser will now refresh, please wait... - Please click finish to complete installation and reload page. - - - Paste with full formatting (Not recommended) - The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. - Paste as raw text without any formatting at all - Paste, but remove formatting (Recommended) - - - Role based protection - using Umbraco's member groups.]]> - You need to create a membergroup before you can use role-based authentication - Error Page - Used when people are logged on, but do not have access - Choose how to restrict access to this page - %0% is now protected - Protection removed from %0% - Login Page - Choose the page that contains the login form - Remove Protection - Select the pages that contain login form and error messages - Pick the roles who have access to this page - Set the login and password for this page - Single user protection - If you just want to setup simple protection using a single login and password - - - - + + Download update from the repository + Upgrade package + Upgrade instructions + There's an upgrade available for this package. You can download it directly from the Umbraco package repository. + Package version + Package version history + View package website + Package already installed + This package cannot be installed, it requires a minimum Umbraco version of + Uninstalling... + Downloading... + Importing... + Installing... + Restarting, please wait... + All done, your browser will now refresh, please wait... + Please click 'Finish' to complete installation and reload the page. + Uploading package... + + + Paste with full formatting (Not recommended) + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + Paste as raw text without any formatting at all + Paste, but remove formatting (Recommended) + + + Role based protection + using Umbraco's member groups.]]> + You need to create a membergroup before you can use role-based authentication + Error Page + Used when people are logged on, but do not have access + Choose how to restrict access to this page + %0% is now protected + Protection removed from %0% + Login Page + Choose the page that contains the login form + Remove Protection + Select the pages that contain login form and error messages + Pick the roles who have access to this page + Set the login and password for this page + Single user protection + If you just want to setup simple protection using a single login and password + + + + - - + + - + + + - + + + - + + + - Include unpublished subpages - Publishing in progress - please wait... - %0% out of %1% pages have been published... - %0% has been published - %0% and subpages have been published - Publish %0% and all its subpages - Publish to publish %0% and thereby making its content publicly available.

+ ]]> +
+ Include unpublished subpages + Publishing in progress - please wait... + %0% out of %1% pages have been published... + %0% has been published + %0% and subpages have been published + Publish %0% and all its subpages + + Publish to publish %0% and thereby making its content publicly available.

You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]>
- - - You have not configured any approved colours - - - enter external link - choose internal page - Caption - Link - New window - Enter a new caption - Enter the link - - - Reset - - - Current version - Red text will not be shown in the selected version. , green means added]]> - Document has been rolled back - This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view - Rollback to - Select version - View - - - Edit script file - - - Concierge - Content - Courier - Developer - Umbraco Configuration Wizard - Media - Members - Newsletters - Settings - Statistics - Translation - Users - Help - Forms - Analytics - Forms - - go to - Help topics for - Video chapters for - The best Umbraco video tutorials - - - Default template - Dictionary Key - To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) - New Tab Title - Node type - Type - Stylesheet - Script - Stylesheet property - Tab - Tab Title - Tabs - Master Content Type enabled - This Content Type uses - as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself - No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. - Master Document Type - Create matching template - Add icon - - - Sort order - Creation date - Sorting complete. - Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items -
Do not close this window during sorting]]>
- - - Validation - Validation errors must be fixed before the item can be saved - Failed - Insufficient user permissions, could not complete the operation - Cancelled - Operation was cancelled by a 3rd party add-in - Publishing was cancelled by a 3rd party add-in - Property type already exists - Property type created - DataType: %1%]]> - Propertytype deleted - Document Type saved - Tab created - Tab deleted - Tab with id: %0% deleted - Stylesheet not saved - Stylesheet saved - Stylesheet saved without any errors - Datatype saved - Dictionary item saved - Publishing failed because the parent page isn't published - Content published - and visible on the website - Content saved - Remember to publish to make changes visible - Sent For Approval - Changes have been sent for approval - Media saved - Media saved without any errors - Member saved - Stylesheet Property Saved - Stylesheet saved - Template saved - Error saving user (check log) - User Saved - User type saved - File not saved - file could not be saved. Please check file permissions - File saved - File saved without any errors - Language saved - Media Type saved - Member Type saved - Python script not saved - Python script could not be saved due to error - Python script saved - No errors in python script - Template not saved - Please make sure that you do not have 2 templates with the same alias - Template saved - Template saved without any errors! - XSLT not saved - XSLT contained an error - XSLT could not be saved, check file permissions - XSLT saved - No errors in XSLT - Content unpublished - Partial view saved - Partial view saved without any errors! - Partial view not saved - An error occurred saving the file. - Script view saved - Script view saved without any errors! - Script view not saved - An error occurred saving the file. - An error occurred saving the file. - - - Uses CSS syntax ex: h1, .redHeader, .blueTex - Edit stylesheet - Edit stylesheet property - Name to identify the style property in the rich text editor - Preview - Styles - - - - Edit template - - Sections - Insert content area - Insert content area placeholder - - Insert - Choose what to insert into your template - - Dictionary item - A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. - - Macro - - A Macro is a configurable component which is great for - reusable parts of your design, where you need the option to provide parameters, - such as galleries, forms and lists. - - - Value - Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. - - Partial view - - A partial view is a separate template file which can be rendered inside another - template, it's great for reusing markup or for separating complex templates into separate files. - - - Master template - No master template - No master - - Render child template - - + + + + You have not configured any approved colours + + + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Deleted item + + + enter external link + choose internal page + Caption + Link + Open in new window + enter the display caption + Enter the link + + + Reset + Define crop + Give the crop an alias and its default width and height + Save crop + Add new crop + + + Current version + Red text will not be shown in the selected version. , green means added]]> + Document has been rolled back + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view + Rollback to + Select version + View + + + Edit script file + + + Concierge + Content + Courier + Developer + Umbraco Configuration Wizard + Media + Members + Newsletters + Settings + Statistics + Translation + Users + Help + Forms + Analytics + + + go to + Help topics for + Video chapters for + The best Umbraco video tutorials + + + Default template + Dictionary Key + To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + New Tab Title + Node type + Type + Stylesheet + Script + Stylesheet property + Tab + Tab Title + Tabs + Master Content Type enabled + This Content Type uses + as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself + No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Master Document Type + Create matching template + Add icon + + + Sort order + Creation date + Sorting complete. + Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items + + + + Validation + Validation errors must be fixed before the item can be saved + Failed + Saved + Insufficient user permissions, could not complete the operation + Cancelled + Operation was cancelled by a 3rd party add-in + Publishing was cancelled by a 3rd party add-in + Property type already exists + Property type created + DataType: %1%]]> + Propertytype deleted + Document Type saved + Tab created + Tab deleted + Tab with id: %0% deleted + Stylesheet not saved + Stylesheet saved + Stylesheet saved without any errors + Datatype saved + Dictionary item saved + Publishing failed because the parent page isn't published + Content published + and visible on the website + Content saved + Remember to publish to make changes visible + Sent For Approval + Changes have been sent for approval + Media saved + Media saved without any errors + Member saved + Stylesheet Property Saved + Stylesheet saved + Template saved + Error saving user (check log) + User Saved + User type saved + User group saved + File not saved + file could not be saved. Please check file permissions + File saved + File saved without any errors + Language saved + Media Type saved + Member Type saved + Python script not saved + Python script could not be saved due to error + Python script saved + No errors in python script + Template not saved + Please make sure that you do not have 2 templates with the same alias + Template saved + Template saved without any errors! + XSLT not saved + XSLT contained an error + XSLT could not be saved, check file permissions + XSLT saved + No errors in XSLT + Content unpublished + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. + Permissions saved for + Script view saved + Script view saved without any errors! + Script view not saved + An error occurred saving the file. + An error occurred saving the file. + Deleted %0% user groups + %0% was deleted + Enabled %0% users + An error occurred while enabling the users + Disabled %0% users + An error occurred while disabling the users + %0% is now enabled + An error occurred while enabling the user + %0% is now disabled + An error occurred while disabling the user + User groups have been set + Deleted %0% user groups + %0% was deleted + Unlocked %0% users + An error occurred while unlocking the users + %0% is now unlocked + An error occurred while unlocking the user + + + Uses CSS syntax ex: h1, .redHeader, .blueTex + Edit stylesheet + Edit stylesheet property + Name to identify the style property in the rich text editor + Preview + Styles + + + + Edit template + + Sections + Insert content area + Insert content area placeholder + + Insert + Choose what to insert into your template + + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + + Master template + No master template + No master + + Render child template + + @RenderBody() placeholder. ]]> - - - - Define a named section - - + + + Define a named section + + @section { ... }. This can be rendered in a specific area of the parent of this template, by using @RenderSection. ]]> - + - Render a named section - - Render a named section + + @RenderSection(name) placeholder. This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. ]]> - - - Section Name - Section is mandatory - - If mandatory, the child template must contain a @section definition, otherwise an error is shown. - - - - Query builder - items returned, in - - I want - all content - content of type "%0%" - from - my website - where - and - - is - is not - before - before (including selected date) - after - after (including selected date) - equals - does not equal - contains - does not contain - greater than - greater than or equal to - less than - less than or equal to - - Id - Name - Created Date - Last Updated Date - - order by - ascending - descending - - Template - - - - - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied - - This content is not allowed here - This content is allowed here - - Click to embed - Click to insert image - Image caption... - Write here... - - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells - - Columns - Total combined number of columns in the grid layout - - Settings - Configure what settings editors can change - - Styles - Configure what styling editors can change - - Settings will only save if the entered json configuration is valid - - Allow all editors - Allow all row configurations - Set as default - Choose extra - Choose default - are added - - - - - Compositions - You have not added any tabs - Add new tab - Add another tab - Inherited from - Add property - Required label - - Enable list view - Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree - - Allowed Templates - Choose which templates editors are allowed to use on content of this type - - Allow as root - Allow editors to create content of this type in the root of the content tree - Yes - allow content of this type in the root - - Allowed child node types - Allow content of the specified types to be created underneath content of this type - - Choose child node - - Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. - This content type is used in a composition, and therefore cannot be composed itself. - There are no content types available to use as a composition. - - Available editors - Reuse - Editor settings - - Configuration - - Yes, delete - - was moved underneath - was copied underneath - Select the folder to move - Select the folder to copy - to in the tree structure below - - All Document types - All Documents - All media items - - using this document type will be deleted permanently, please confirm you want to delete these as well. - using this media type will be deleted permanently, please confirm you want to delete these as well. - using this member type will be deleted permanently, please confirm you want to delete these as well - - and all documents using this type - and all media items using this type - and all members using this type - - using this editor will get updated with the new settings - - Member can edit - Show on member profile - - - - - Alternative field - Alternative Text - Casing - Encoding - Choose field - Convert line breaks - Replaces line breaks with html-tag &lt;br&gt; - Custom Fields - Yes, Date only - Format as date - HTML encode - Will replace special characters by their HTML equivalent. - Will be inserted after the field value - Will be inserted before the field value - Lowercase - None - Insert after field - Insert before field - Recursive - Remove Paragraph tags - Will remove any &lt;P&gt; in the beginning and end of the text - Standard Fields - Uppercase - URL encode - Will format special characters in URLs - Will only be used when the field values above are empty - This field will only be used if the primary field is empty - Yes, with time. Separator: - - - Tasks assigned to you - assigned to you. To see a detailed view including comments, click on "Details" or just the page name. + + + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + + + Query builder + Build a query + items returned, in + + I want + all content + content of type "%0%" + from + my website + where + and + + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + + Id + Name + Created Date + Last Updated Date + + order by + ascending + descending + + Template + + + + + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + Leave blank or set to 0 for unlimited + Maximum items + Set as default + Choose extra + Choose default + are added + + + + + Compositions + You have not added any tabs + Add new tab + Add another tab + Inherited from + Add property + Required label + + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + + Allowed Templates + Choose which templates editors are allowed to use on content of this type + + Allow as root + Allow editors to create content of this type in the root of the content tree + Yes - allow content of this type in the root + + Allowed child node types + Allow content of the specified types to be created underneath content of this type + + Choose child node + + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + + Available editors + Reuse + Editor settings + + Configuration + + Yes, delete + + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + + All Document types + All Documents + All media items + + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + + and all documents using this type + and all media items using this type + and all members using this type + + using this editor will get updated with the new settings + + Member can edit + Show on member profile + tab has no sort order + + + + Building models + this can take a bit of time, don't worry + Models generated + Models could not be generated + Models generation has failed, see exception in U log + + + + Add fallback field + Fallback field + Add default value + Default value + Fallback field + Default value + Casing + Encoding + Choose field + Convert line breaks + Yes, convert line breaks + Replaces line breaks with 'br' html tag + Custom Fields + Date only + Format and encoding + Format as date + Format the value as a date, or a date with time, according to the active culture + HTML encode + Will replace special characters by their HTML equivalent. + Will be inserted after the field value + Will be inserted before the field value + Lowercase + Modify output + None + Output sample + Insert after field + Insert before field + Recursive + Yes, make it recursive + Separator + Standard Fields + Uppercase + URL encode + Will format special characters in URLs + Will only be used when the field values above are empty + This field will only be used if the primary field is empty + Date and time + + + Tasks assigned to you + + assigned to you. To see a detailed view including comments, click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link.
To close a translation task, please go to the Details view and click the "Close" button. - ]]>
- close task - Translation details - Download all translation tasks as XML - Download XML - Download XML DTD - Fields - Include subpages - + + close task + Translation details + Download all translation tasks as XML + Download XML + Download XML DTD + Fields + Include subpages + + - [%0%] Translation task for %1% - No translator users found. Please create a translator user before you start sending content to translation - Tasks created by you - created by you. To see a detailed view including comments, + ]]> + + [%0%] Translation task for %1% + No translator users found. Please create a translator user before you start sending content to translation + Tasks created by you + + created by you. To see a detailed view including comments, click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link. To close a translation task, please go to the Details view and click the "Close" button. - ]]> - The page '%0%' has been send to translation - Please select the language that the content should be translated into - Send the page '%0%' to translation - Assigned by - Task opened - Total words - Translate to - Translation completed. - You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. - Translation failed, the XML file might be corrupt - Translation options - Translator - Upload translation XML - - - Cache Browser - Recycle Bin - Created packages - Data Types - Dictionary - Installed packages - Install skin - Install starter kit - Languages - Install local package - Macros - Media Types - Members - Member Groups - Roles - Member Types - Document Types - Relation Types - Packages - Packages - Python Files - Install from repository - Install Runway - Runway modules - Scripting Files - Scripts - Stylesheets - Templates - XSLT Files - Analytics - Partial Views - Partial View Macro Files - - - New update ready - %0% is ready, click here for download - No connection to server - Error checking for update. Please review trace-stack for further information - - - Administrator - Category field - Change Your Password - New password - Confirm new password - You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button - Content Channel - Description field - Disable User - Document Type - Editor - Excerpt field - Language - Username - Start Node in Media Library - Sections - Disable Umbraco Access - Old password - Password - Reset password - Your password has been changed! - Please confirm the new password - Enter your new password - Your new password cannot be blank! - Current password - Invalid current password - There was a difference between the new password and the confirmed password. Please try again! - The confirmed password doesn't match the new password! - Replace child node permissions - You are currently modifying permissions for the pages: - Select pages to modify their permissions - Search all children - Start Node in Content - Name - User permissions - User type - User types - Writer - Translator - Change - Your profile - Your recent history - Session expires in - - - Validation - Validate as email - Validate as a number - Validate as a Url - ...or enter a custom validation - Field is mandatory - - - +
+ + + + + + + + + + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ You have been invited by %1% to the Umbraco Back Office. +

+

+ Message from %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Click this link to accept the invite + +
+
+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %3% + +
+

+
+
+


+
+
+ + ]]> +
+ + + + + Validation + Validate as email + Validate as a number + Validate as a Url + ...or enter a custom validation + Field is mandatory + Enter a regular expression + You need to add at least + You can only have + items + items selected + Invalid date + Not a number + Invalid email + + + - Value is set to the recommended value: '%0%'. - Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. - Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. - Found unexpected value '%0%' for '%2%' in configuration file '%3%'. + Value is set to the recommended value: '%0%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. + Found unexpected value '%0%' for '%2%' in configuration file '%3%'. - - Custom errors are set to '%0%'. - Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. - Custom errors successfully set to '%0%'. + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. - MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. - MacroErrors are now set to '%0%'. + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. - - Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. - Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). - Try Skip IIS Custom Errors successfully set to '%0%'. - - - File does not exist: '%0%'. - '%0%' in config file '%1%'.]]> - There was an error, check log for full error: %0%. - - Members - Total XML: %0%, Total: %1%, Total invalid: %2% - Media - Total XML: %0%, Total: %1%, Total invalid: %2% - Content - Total XML: %0%, Total published: %1%, Total invalid: %2% - - Your site certificate was marked as valid. - Certificate validation error: '%0%' - Error pinging the URL %0% - '%1%' - You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0% - - - Enable HTTPS - Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure. - - Fix - Cannot fix a check with a value comparison type of 'ShouldNotEqual'. - Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. - Value to fix check not provided. - - Debug compilation mode is disabled. - Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. - Debug compilation mode successfully disabled. - - Trace mode is disabled. - Trace mode is currently enabled. It is recommended to disable this setting before go live. - Trace mode successfully disabled. - - All folders have the correct permissions set. - + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + + Members - Total XML: %0%, Total: %1%, Total invalid: %2% + Media - Total XML: %0%, Total: %1%, Total invalid: %2% + Content - Total XML: %0%, Total published: %1%, Total invalid: %2% + + Your website's certificate is valid. + Certificate validation error: '%0%' + Your website's SSL certificate has expired. + Your website's SSL certificate is expiring in %0% days. + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0% + + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure. + + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. + + All folders have the correct permissions set. + - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> - All files have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> - Set Header in Config - Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. - A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. - Could not update web.config file. Error: %0% + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + Set Header in Config + Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. + A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. + Could not update web.config file. Error: %0% - - %0%.]]> - No headers revealing information about the website technology were found. - - In the Web.config file, system.net/mailsettings could not be found. - In the Web.config file system.net/mailsettings section, the host is not configured. - SMTP settings are configured correctly and the service is operating as expected. - The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. - - %0%.]]> - %0%.]]> - - - Disable URL tracker - Enable URL tracker - Original URL - Redirected To - No redirects have been made - When a published page gets renamed or moved a redirect will automatically be made to the new page. - Remove - Are you sure you want to remove the redirect from '%0%' to '%1%'? - Redirect URL removed. - Error removing redirect URL. - Are you sure you want to disable the URL tracker? - URL tracker has now been disabled. - Error disabling the URL tracker, more information can be found in your log file. - URL tracker has now been enabled. - Error enabling the URL tracker, more information can be found in your log file. - - - No Dictionary items to choose from - -
+ %0%.]]> + No headers revealing information about the website technology were found. + + In the Web.config file, system.net/mailsettings could not be found. + In the Web.config file system.net/mailsettings section, the host is not configured. + SMTP settings are configured correctly and the service is operating as expected. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + + %0%.]]> + %0%.]]> +

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
+ Umbraco Health Check Status + + + Disable URL tracker + Enable URL tracker + Original URL + Redirected To + No redirects have been made + When a published page gets renamed or moved a redirect will automatically be made to the new page. + Remove + Are you sure you want to remove the redirect from '%0%' to '%1%'? + Redirect URL removed. + Error removing redirect URL. + Are you sure you want to disable the URL tracker? + URL tracker has now been disabled. + Error disabling the URL tracker, more information can be found in your log file. + URL tracker has now been enabled. + Error enabling the URL tracker, more information can be found in your log file. + + + No Dictionary items to choose from + + + characters left + + \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/en_us.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/en_us.xml index 95630f03..ba4d9d89 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/en_us.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/en_us.xml @@ -1,666 +1,789 @@ - - The Umbraco community - http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files - - - Culture and Hostnames - Audit Trail - Browse Node - Change Document Type - Copy - Create - Create Package - Delete - Disable - Empty recycle bin - Export Document Type - Import Document Type - Import Package - Edit in Canvas - Exit - Move - Notifications - Public access - Publish - Unpublish - Reload - Republish entire site - Set permissions for the page %0% - Choose where to move - to in the tree structure below - Restore - Permissions - Rollback - Send To Publish - Send To Translation - Sort - Send to publication - Translate - Update - Default value - - - Permission denied. - Add new Domain - remove - Invalid node. - Invalid domain format. - Domain has already been assigned. - Language - Domain - New domain '%0%' has been created - Domain '%0%' is deleted - Domain '%0%' has already been assigned - Domain '%0%' has been updated - Edit Current Domains - + The Umbraco community + http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files + + + Culture and Hostnames + Audit Trail + Browse Node + Change Document Type + Copy + Create + Create Package + Create group + Delete + Disable + Empty recycle bin + Enable + Export Document Type + Import Document Type + Import Package + Edit in Canvas + Exit + Move + Notifications + Public access + Publish + Unpublish + Reload + Republish entire site + Rename + Restore + Set permissions for the page %0% + Choose where to move + In the tree structure below + Permissions + Rollback + Send To Publish + Send To Translation + Set group + Sort + Translate + Update + Set permissions + Unlock + Create Content Template + + + Content + Administration + Structure + Other + + + Allow access to assign culture and hostnames + Allow access to view a node's history log + Allow access to view a node + Allow access to change document type for a node + Allow access to copy a node + Allow access to create nodes + Allow access to delete nodes + Allow access to move a node + Allow access to set and change public access for a node + Allow access to publish a node + Allow access to change permissions for a node + Allow access to roll back a node to a previous state + Allow access to send a node for approval before publishing + Allow access to send a node for translation + Allow access to change the sort order for nodes + Allow access to translate a node + Allow access to save a node + Allow access to create a Content Template + + + Permission denied. + Add new Domain + remove + Invalid node. + Invalid domain format. + Domain has already been assigned. + Language + Domain + New domain '%0%' has been created + Domain '%0%' is deleted + Domain '%0%' has already been assigned + Domain '%0%' has been updated + Edit Current Domains + + - Inherit - Culture - or inherit culture from parent nodes. Will also apply
- to the current node, unless a domain below applies too.]]>
- Domains - - - Viewing for - - - Clear selection - Select - Select current folder - Do something else - Bold - Cancel Paragraph Indent - Insert form field - Insert graphic headline - Edit Html - Indent Paragraph - Italic - Center - Justify Left - Justify Right - Insert Link - Insert local link (anchor) - Bullet List - Numeric List - Insert macro - Insert picture - Edit relations - Return to list - Save - Save and publish - Save and send for approval - Save list view - Preview - Preview is disabled because there's no template assigned - Choose style - Show styles - Insert table - Generate models - Save and generate models - - - To change the document type for the selected content, first select from the list of valid types for this location. - Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. - The content has been re-published. - Current Property - Current type - The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. - Document Type Changed - Map Properties - Map to Property - New Template - New Type - none - Content - Select New Document Type - The document type of the selected content has been successfully changed to [new type] and the following properties mapped: - to - Could not complete property mapping as one or more properties have more than one mapping defined. - Only alternate types valid for the current location are displayed. - - - Is Published - About this page - Alias - (how would you describe the picture over the phone) - Alternative Links - Click to edit this item - Created by - Original author - Updated by - Created - Date/time this document was created - Document Type - Editing - Remove at - This item has been changed after publication - This item is not published - Last published - There are no items to show - There are no items to show in the list. - Media Type - Link to media item(s) - Member Group - Role - Member Type - No date chosen - Link title - Properties - This document is published but is not visible because the parent '%0%' is unpublished - This document is published but is not in the cache - Could not get the url - This document is published but its url would collide with content %0% - Publish - Publication Status - Publish at - Unpublish at - Clear Date - Sortorder is updated - To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting - Statistics - Title (optional) - Alternative text (optional) - Type - Unpublish - Last edited - Date/time this document was edited - Remove file(s) - Link to document - Member of group(s) - Not a member of group(s) - Child items - Target - This translates to the following time on the server: - What does this mean?]]> - - - Click to upload - Drop your files here... - Link to media - or click here to choose files - Only allowed file types are - Cannot upload this file, it does not have an approved file type - Max file size is - - - Create a new member - All Members - - - Where do you want to create the new %0% - Create an item under - Choose a type and a title - "document types".]]> - "media types".]]> - Document Type without a template - New folder - New data type - New javascript file - New empty partial view - New partial view macro - New partial view from snippet - New empty partial view macro - New partial view macro from snippet - New partial view macro (without macro) - - - Browse your website - - Hide - If Umbraco isn't opening, you might need to allow popups from this site - has opened in a new window - Restart - Visit - Welcome - - - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes - - - Done - - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items - - - Name - Manage hostnames - Close this window - Are you sure you want to delete - Are you sure you want to disable - Please check this box to confirm deletion of %0% item(s) - Are you sure? - Are you sure? - Cut - Edit Dictionary Item - Edit Language - Insert local link - Insert character - Insert graphic headline - Insert picture - Insert link - Click to add a Macro - Insert table - Last Edited - Link - Internal link: - When using local links, insert "#" in front of link - Open in new window? - Macro Settings - This macro does not contain any properties you can edit - Paste - Edit Permissions for - The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place - The recycle bin is now empty - When items are deleted from the recycle bin, they will be gone forever - regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> - Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' - Remove Macro - Required Field - Site is reindexed - The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished - The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. - Number of columns - Number of rows - Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, - by referring this ID using a <asp:content /> element.]]> - Select a placeholder id from the list below. You can only - choose Id's from the current template's master.]]> - Click on the image to see full size - Pick item - View Cache Item - Create folder... - Relate to original - Include descendants - The friendliest community - Link to page - Opens the linked document in a new window or tab - Link to media - Select media - Select icon - Select item - Select link - Select macro - Select content - Select member - Select member group - There are no parameters for this macro - External login providers - Exception Details - Stacktrace - Inner Exception - Link your - Un-Link your - account - Select editor - - - + + Inherit + Culture + + or inherit culture from parent nodes. Will also apply
+ to the current node, unless a domain below applies too.]]> +
+ Domains + + + Viewing for + + + Clear selection + Select + Select current folder + Do something else + Bold + Cancel Paragraph Indent + Insert form field + Insert graphic headline + Edit Html + Indent Paragraph + Italic + Center + Justify Left + Justify Right + Insert Link + Insert local link (anchor) + Bullet List + Numeric List + Insert macro + Insert picture + Edit relations + Return to list + Save + Save and publish + Save and send for approval + Save list view + Preview + Preview is disabled because there's no template assigned + Choose style + Show styles + Insert table + Generate models + Save and generate models + Undo + Redo + + + To change the document type for the selected content, first select from the list of valid types for this location. + Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. + The content has been re-published. + Current Property + Current type + The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. + Document Type Changed + Map Properties + Map to Property + New Template + New Type + none + Content + Select New Document Type + The document type of the selected content has been successfully changed to [new type] and the following properties mapped: + to + Could not complete property mapping as one or more properties have more than one mapping defined. + Only alternate types valid for the current location are displayed. + + + Is Published + About this page + Alias + (how would you describe the picture over the phone) + Alternative Links + Click to edit this item + Created by + Original author + Updated by + Created + Date/time this document was created + Document Type + Editing + Remove at + This item has been changed after publication + This item is not published + Last published + There are no items to show + There are no items to show in the list. + No content has been added + No members have been added + Media Type + Link to media item(s) + Member Group + Role + Member Type + No date chosen + Page title + Properties + This document is published but is not visible because the parent '%0%' is unpublished + This document is published but is not in the cache + Could not get the url + This document is published but its url would collide with content %0% + Publish + Publication Status + Publish at + Unpublish at + Clear Date + Sortorder is updated + To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting + Statistics + Title (optional) + Alternative text (optional) + Type + Unpublish + Last edited + Date/time this document was edited + Remove file(s) + Link to document + Member of group(s) + Not a member of group(s) + Child items + Target + This translates to the following time on the server: + What does this mean?]]> + Are you sure you want to delete this item? + Property %0% uses editor %1% which is not supported by Nested Content. + Add another text box + Remove this text box + Content root + + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is pre-defined content that an editor can select to use as the basis for creating new content + + + Click to upload + Drop your files here... + Link to media + or click here to choose files + Only allowed file types are + Cannot upload this file, it does not have an approved file type + Max file size is + Media root + + + Create a new member + All Members + + + Where do you want to create the new %0% + Create an item under + Select the document type you want to make a content template for + Choose a type and a title + "document types".]]> + "media types".]]> + Document Type without a template + New folder + New data type + New javascript file + New empty partial view + New partial view macro + New partial view from snippet + New empty partial view macro + New partial view macro from snippet + New partial view macro (without macro) + + + Browse your website + - Hide + If Umbraco isn't opening, you might need to allow popups from this site + has opened in a new window + Restart + Visit + Welcome + + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + + + Done + + Deleted %0% item + Deleted %0% items + Deleted %0% out of %1% item + Deleted %0% out of %1% items + + Published %0% item + Published %0% items + Published %0% out of %1% item + Published %0% out of %1% items + + Unpublished %0% item + Unpublished %0% items + Unpublished %0% out of %1% item + Unpublished %0% out of %1% items + + Moved %0% item + Moved %0% items + Moved %0% out of %1% item + Moved %0% out of %1% items + + Copied %0% item + Copied %0% items + Copied %0% out of %1% item + Copied %0% out of %1% items + + + Link title + Link + Name + Manage hostnames + Close this window + Are you sure you want to delete + Are you sure you want to disable + Please check this box to confirm deletion of %0% item(s) + Are you sure? + Are you sure? + Cut + Edit Dictionary Item + Edit Language + Insert local link + Insert character + Insert graphic headline + Insert picture + Insert link + Click to add a Macro + Insert table + Last Edited + Link + Internal link: + When using local links, insert "#" in front of link + Open in new window? + Macro Settings + This macro does not contain any properties you can edit + Paste + Edit permissions for + Set permissions for + Set permissions for %0% for user group %1% + Select the users groups you want to set permissions for + The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place + The recycle bin is now empty + When items are deleted from the recycle bin, they will be gone forever + regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> + Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' + Remove Macro + Required Field + Site is reindexed + The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished + The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. + Number of columns + Number of rows + + Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, + by referring this ID using a <asp:content /> element.]]> + + + Select a placeholder id from the list below. You can only + choose Id's from the current template's master.]]> + + Click on the image to see full size + Pick item + View Cache Item + Create folder... + Relate to original + Include descendants + The friendliest community + Link to page + Opens the linked document in a new window or tab + Link to media + Link to file + Select content start node + Select media + Select icon + Select item + Select link + Select macro + Select content + Select media start node + Select member + Select member group + Select node + Select sections + Select users + No icons were found + There are no parameters for this macro + There are no macros available to insert + External login providers + Exception Details + Stacktrace + Inner Exception + Link your + Un-link your + account + Select editor + Select snippet + + + + %0%' below
You can add additional languages under the 'languages' in the menu on the left - ]]>
- Culture Name - Edit the key of the dictionary item. - - + + Culture Name + Edit the key of the dictionary item. + + - - - - Enter your username - Enter your password - Confirm your password - Name the %0%... - Enter a name... - Label... - Enter a description... - Type to search... - Type to filter... - Type to add tags (press enter after each tag)... - Enter your email - - - - Allow at root - Only Content Types with this checked can be created at the root level of Content and Media trees - Allowed child node types - Document Type Compositions - Create - Delete tab - Description - New tab - Tab - Thumbnail - Enable list view - Configures the content item to show a sortable & searchable list of its children, the children will not be shown in the tree - Current list view - The active list view data type - Create custom list view - Remove custom list view - - - Add prevalue - Database datatype - Property editor GUID - Property editor - Buttons - Enable advanced settings for - Enable context menu - Maximum default size of inserted images - Related stylesheets - Show label - Width and height - - - Your data has been saved, but before you can publish this page there are some errors you need to fix first: - The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) - %0% already exists - There were errors: - There were errors: - The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) - %0% must be an integer - The %0% field in the %1% tab is mandatory - %0% is a mandatory field - %0% at %1% is not in a correct format - %0% is not in a correct format - - - Received an error from the server - The specified file type has been disallowed by the administrator - NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. - Please fill both alias and name on the new property type! - There is a problem with read/write access to a specific file or folder - Error loading Partial View script (file: %0%) - Error loading userControl '%0%' - Error loading customControl (Assembly: %0%, Type: '%1%') - Error loading MacroEngine script (file: %0%) - "Error parsing XSLT file: %0% - "Error reading XSLT file: %0% - Please enter a title - Please choose a type - You're about to make the picture larger than the original size. Are you sure that you want to proceed? - Error in python script - The python script has not been saved, because it contained error(s) - Startnode deleted, please contact your administrator - Please mark content before changing style - No active styles available - Please place cursor at the left of the two cells you wish to merge - You cannot split a cell that hasn't been merged. - Error in XSLT source - The XSLT has not been saved, because it contained error(s) - There is a configuration error with the data type used for this property, please check the data type - - - About - Action - Actions - Add - Alias - All - Are you sure? - Back - Border - by - Cancel - Cell margin - Choose - Close - Close Window - Comment - Confirm - Constrain proportions - Continue - Copy - Create - Database - Date - Default - Delete - Deleted - Deleting... - Design - Dimensions - Down - Download - Edit - Edited - Elements - Email - Error - Find - Height - Help - Icon - Import - Inner margin - Insert - Install - Invalid - Justify - Label - Language - Layout - Loading - Locked - Login - Log off - Logout - Macro - Mandatory - Move - More - Name - New - Next - No - of - OK - Open - or - Password - Path - Placeholder ID - One moment please... - Previous - Properties - Email to receive form data - Recycle Bin - Your recycle bin is empty - Remaining - Rename - Renew - Required - Retry - Permissions - Search - Sorry, we can not find what you are looking for - Server - Show - Show page on Send - Size - Sort - Submit - Type - Type to search... - Up - Update - Upgrade - Upload - Url - User - Username - Value - View - Welcome... - Width - Yes - Folder - Search results - Reorder - I am done reordering - Preview - Change password - to - List view - Saving... - current - Embed - selected - - - Black - Green - Yellow - Orange - Blue - Red - - - Add tab - Add property - Add editor - Add template - Add child node - Add child - - Edit data type - - Navigate sections - - Shortcuts - show shortcuts - - Toggle list view - Toggle allow as root - - Comment/Uncomment lines - Remove line - Copy Lines Up - Copy Lines Down - Move Lines Up - Move Lines Down - - General - Editor - - - Background color - Bold - Text color - Font - Text - - - Page - - - The installer cannot connect to the database. - Could not save the web.config file. Please modify the connection string manually. - Your database has been found and is identified as - Database configuration - + + + Enter your username + Enter your password + Confirm your password + Name the %0%... + Enter a name... + Enter an email... + Enter a username... + Label... + Enter a description... + Type to search... + Type to filter... + Type to add tags (press enter after each tag)... + Enter your email... + Enter a message... + Your username is usually your email + + + Allow at root + Only Content Types with this checked can be created at the root level of Content and Media trees + Allowed child node types + Document Type Compositions + Create + Delete tab + Description + New tab + Tab + Thumbnail + Enable list view + Configures the content item to show a sortable & searchable list of its children, the children will not be shown in the tree + Current list view + The active list view data type + Create custom list view + Remove custom list view + + + Renamed + Enter a new folder name here + %0% was renamed to %1% + + + Add prevalue + Database datatype + Property editor GUID + Property editor + Buttons + Enable advanced settings for + Enable context menu + Maximum default size of inserted images + Related stylesheets + Show label + Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath + + + Your data has been saved, but before you can publish this page there are some errors you need to fix first: + The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) + %0% already exists + There were errors: + There were errors: + The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) + %0% must be an integer + The %0% field in the %1% tab is mandatory + %0% is a mandatory field + %0% at %1% is not in a correct format + %0% is not in a correct format + + + Received an error from the server + The specified file type has been disallowed by the administrator + NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. + Please fill both alias and name on the new property type! + There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Error loading userControl '%0%' + Error loading customControl (Assembly: %0%, Type: '%1%') + Error loading MacroEngine script (file: %0%) + "Error parsing XSLT file: %0% + "Error reading XSLT file: %0% + Please enter a title + Please choose a type + You're about to make the picture larger than the original size. Are you sure that you want to proceed? + Error in python script + The python script has not been saved, because it contained error(s) + Startnode deleted, please contact your administrator + Please mark content before changing style + No active styles available + Please place cursor at the left of the two cells you wish to merge + You cannot split a cell that hasn't been merged. + Error in XSLT source + The XSLT has not been saved, because it contained error(s) + There is a configuration error with the data type used for this property, please check the data type + + + About + Action + Actions + Add + Alias + All + Are you sure? + Back + Border + by + Cancel + Cell margin + Choose + Close + Close Window + Comment + Confirm + Constrain + Constrain proportions + Continue + Copy + Create + Database + Date + Default + Delete + Deleted + Deleting... + Design + Dictionary + Dimensions + Down + Download + Edit + Edited + Elements + Email + Error + Find + First + Groups + Height + Help + Hide + Icon + Import + Inner margin + Insert + Install + Invalid + Justify + Label + Language + Last + Layout + Loading + Locked + Login + Log off + Logout + Macro + Mandatory + Message + Move + More + Name + New + Next + No + of + Off + OK + Open + On + or + Order by + Password + Path + Placeholder ID + One moment please... + Previous + Properties + Email to receive form data + Recycle Bin + Your recycle bin is empty + Remaining + Remove + Rename + Renew + Required + Retrieve + Retry + Permissions + Search + Sorry, we can not find what you are looking for + No items have been added + Server + Show + Show page on Send + Size + Sort + Status + Submit + Type + Type to search... + Up + Update + Upgrade + Upload + Url + User + Username + Value + View + Welcome... + Width + Yes + Folder + Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + Retrieve + selected + + + Black + Green + Yellow + Orange + Blue + Red + + + Add tab + Add property + Add editor + Add template + Add child node + Add child + + Edit data type + + Navigate sections + + Shortcuts + show shortcuts + + Toggle list view + Toggle allow as root + + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor + + + Background color + Bold + Text color + Font + Text + + + Page + + + The installer cannot connect to the database. + Could not save the web.config file. Please modify the connection string manually. + Your database has been found and is identified as + Database configuration + + install button to install the Umbraco %0% database - ]]> - Next to proceed.]]> - Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

+ ]]> +
+ Next to proceed.]]> + + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

Click the retry button when done.
- More information on editing web.config here.

]]>
- + More information on editing web.config here.

]]> +
+ + Please contact your ISP if necessary. - If you're installing on a local machine or server you might need information from your system administrator.]]> - + + + Press the upgrade button to upgrade your database to Umbraco %0%

Don't worry - no content will be deleted and everything will continue working afterwards!

- ]]>
- - Press Next to + ]]> + + + Press Next to proceed. ]]> - - next to continue the configuration wizard]]> - The Default users' password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> - The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> - The password is changed! - - ('admin') and password ('default'). It's important that the password is changed to something unique. - ]]> - Get a great start, watch our introduction videos - By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. - Not installed yet. - Affected files and folders - More information on setting up permissions for Umbraco here - You need to grant ASP.NET modify permissions to the following files/folders - Your permission settings are almost perfect!

- You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
- How to Resolve - Click here to read the text version - video tutorial on setting up folder permissions for Umbraco or read the text version.]]> - Your permission settings might be an issue! + + next to continue the configuration wizard]]> + The Default users' password needs to be changed!]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> + The password is changed! + Get a great start, watch our introduction videos + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. + Not installed yet. + Affected files and folders + More information on setting up permissions for Umbraco here + You need to grant ASP.NET modify permissions to the following files/folders + + Your permission settings are almost perfect!

+ You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]> +
+ How to Resolve + Click here to read the text version + video tutorial on setting up folder permissions for Umbraco or read the text version.]]> + + Your permission settings might be an issue!

- You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
- Your permission settings are not ready for Umbraco! + You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]> + + + Your permission settings are not ready for Umbraco!

- In order to run Umbraco, you'll need to update your permission settings.]]>
- Your permission settings are perfect!

- You are ready to run Umbraco and install packages!]]>
- Resolving folder issue - Follow this link for more information on problems with ASP.NET and creating folders - Setting up folder permissions - + + + Your permission settings are perfect!

+ You are ready to run Umbraco and install packages!]]> +
+ Resolving folder issue + Follow this link for more information on problems with ASP.NET and creating folders + Setting up folder permissions + + - I want to start from scratch - - + + I want to start from scratch + + learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> - You've just set up a clean Umbraco platform. What do you want to do next? - Runway is installed - + + You've just set up a clean Umbraco platform. What do you want to do next? + Runway is installed + + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> - Only recommended for experienced users - I want to start with a simple website - - + + Only recommended for experienced users + I want to start with a simple website + + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, @@ -671,87 +794,181 @@ Included with Runway: Home page, Getting Started page, Installing Modules page.
Optional Modules: Top Navigation, Sitemap, Contact, Gallery. - ]]>
- What is Runway - Step 1/5 Accept license - Step 2/5: Database configuration - Step 3/5: Validating File Permissions - Step 4/5: Check Umbraco security - Step 5/5: Umbraco is ready to get you started - Thank you for choosing Umbraco - Browse your new site -You installed Runway, so why not see how your new website looks.]]> - Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> - Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, -you can find plenty of resources on our getting started pages.]]>
- Launch Umbraco -To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> - Connection to database failed. - Umbraco Version 3 - Umbraco Version 4 - Watch - Umbraco %0% for a fresh install or upgrading from version 3.0. + ]]> + + What is Runway + Step 1/5 Accept license + Step 2/5: Database configuration + Step 3/5: Validating File Permissions + Step 4/5: Check Umbraco security + Step 5/5: Umbraco is ready to get you started + Thank you for choosing Umbraco + + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + + + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + + Umbraco %0% is installed and ready for use + + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + + + started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]> +
+ + Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + + Connection to database failed. + Umbraco Version 3 + Umbraco Version 4 + Watch + + Umbraco %0% for a fresh install or upgrading from version 3.0.

- Press "next" to start the wizard.]]>
- - - Culture Code - Culture Name - - - You've been idle and logout will automatically occur in - Renew now to save your work - - - Happy super Sunday - Happy manic Monday - Happy tubular Tuesday - Happy wonderful Wednesday - Happy thunderous Thursday - Happy funky Friday - Happy Caturday - Log in below - Sign in with - Session timed out - © 2001 - %0%
Umbraco.com

]]>
- Forgotten password? - An email will be sent to the address specified with a link to reset your password - An email with password reset instructions will be sent to the specified address if it matched our records - Return to login form - Please provide a new password - Your Password has been updated - The link you have clicked on is invalid or has expired - Umbraco: Reset Password - - Your username to login to the Umbraco back-office is: %0%

Click here to reset your password or copy/paste this URL into your browser:

%1%

]]> -
- - - Dashboard - Sections - Content - - - Choose page above... - %0% has been copied to %1% - Select where the document %0% should be copied to below - %0% has been moved to %1% - Select where the document %0% should be moved to below - has been selected as the root of your new content, click 'ok' below. - No node selected yet, please select a node in the list above before clicking 'ok' - The current node is not allowed under the chosen node because of its type - The current node cannot be moved to one of its subpages - The current node cannot exist at the root - The action isn't allowed since you have insufficient permissions on 1 or more child documents. - Relate copied items to original - - - Edit your notification for %0% - "next" to start the wizard.]]> + + + + Culture Code + Culture Name + + + You've been idle and logout will automatically occur in + Renew now to save your work + + + Happy super Sunday + Happy manic Monday + Happy tubular Tuesday + Happy wonderful Wednesday + Happy thunderous Thursday + Happy funky Friday + Happy Caturday + Log in below + Sign in with + Session timed out + © 2001 - %0%
Umbraco.com

]]>
+ Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Password reset requested +

+

+ Your username to login to the Umbraco back-office is: %0% +

+

+ + + + + + +
+ + Click this link to reset your password + +
+

+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %1% + +
+

+
+
+


+
+
+ + + ]]> +
+ + + Dashboard + Sections + Content + + + Choose page above... + %0% has been copied to %1% + Select where the document %0% should be copied to below + %0% has been moved to %1% + Select where the document %0% should be moved to below + has been selected as the root of your new content, click 'ok' below. + No node selected yet, please select a node in the list above before clicking 'ok' + The current node is not allowed under the chosen node because of its type + The current node cannot be moved to one of its subpages + The current node cannot exist at the root + The action isn't allowed since you have insufficient permissions on 1 or more child documents. + Relate copied items to original + + + Edit your notification for %0% + + - - Hi %0%

- -

This is an automated mail to inform you that the task '%1%' - has been performed on the page '%2%' - by the user '%3%' -

- -

-

Update summary:

- - %6% + ]]> + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ +
+
+ + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' +

+ + + + + + +
+ +
+ EDIT
+
+

+

Update summary:

+ + %6% +
+

+

+ Have a nice day!

+ Cheers from the Umbraco robot +

+
+
+


+
+
-

- - - -

Have a nice day!

- Cheers from the Umbraco robot -

]]>
- [%0%] Notification about %1% performed on %2% - Notifications - - - + + ]]> + + [%0%] Notification about %1% performed on %2% + Notifications + + + + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> - Author - Demonstration - Documentation - Package meta data - Package name - Package doesn't contain any items -
- You can safely remove this from the system by clicking "uninstall package" below.]]>
- No upgrades available - Package options - Package readme - Package repository - Confirm uninstall - Package was uninstalled - The package was successfully uninstalled - Uninstall package - + ]]> + + Drop to upload + or click here to choose package file + Upload package + Install a local package by selecting it from your machine. Only install packages from sources you know and trust + Upload another package + Cancel and upload another package + License + I accept + terms of use + Install package + Finish + Installed packages + You don’t have any packages installed + 'Packages' icon in the top right of your screen]]> + Search for packages + Results for + We couldn’t find anything for + Please try searching for another package or browse through the categories + Popular + New releases + has + karma points + Information + Owner + Contributors + Created + Current version + .NET version + Downloads + Likes + Compatibility + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100% + External sources + Author + Demonstration + Documentation + Package meta data + Package name + Package doesn't contain any items + +
+ You can safely remove this from the system by clicking "uninstall package" below.]]> +
+ No upgrades available + Package options + Package readme + Package repository + Confirm package uninstall + Package was uninstalled + The package was successfully uninstalled + Uninstall package + + Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, - so uninstall with caution. If in doubt, contact the package author.]]> - Download update from the repository - Upgrade package - Upgrade instructions - There's an upgrade available for this package. You can download it directly from the Umbraco package repository. - Package version - Package version history - View package website - Package already installed - This package cannot be installed, it requires a minimum Umbraco version of %0% - Uninstalling... - Downloading... - Importing... - Installing... - Restarting, please wait... - All done, your browser will now refresh, please wait... - Please click finish to complete installation and reload page. - - - Paste with full formatting (Not recommended) - The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. - Paste as raw text without any formatting at all - Paste, but remove formatting (Recommended) - - - Role based protection - using Umbraco's member groups.]]> - You need to create a membergroup before you can use role-based authentication - Error Page - Used when people are logged on, but do not have access - Choose how to restrict access to this page - %0% is now protected - Protection removed from %0% - Login Page - Choose the page that contains the login form - Remove Protection - Select the pages that contain login form and error messages - Pick the roles who have access to this page - Set the login and password for this page - Single user protection - If you just want to setup simple protection using a single login and password - - - - + + Download update from the repository + Upgrade package + Upgrade instructions + There's an upgrade available for this package. You can download it directly from the Umbraco package repository. + Package version + Package version history + View package website + Package already installed + This package cannot be installed, it requires a minimum Umbraco version of + Uninstalling... + Downloading... + Importing... + Installing... + Restarting, please wait... + All done, your browser will now refresh, please wait... + Please click 'Finish' to complete installation and reload the page. + Uploading package... + + + Paste with full formatting (Not recommended) + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + Paste as raw text without any formatting at all + Paste, but remove formatting (Recommended) + + + Role based protection + using Umbraco's member groups.]]> + You need to create a membergroup before you can use role-based authentication + Error Page + Used when people are logged on, but do not have access + Choose how to restrict access to this page + %0% is now protected + Protection removed from %0% + Login Page + Choose the page that contains the login form + Remove Protection + Select the pages that contain login form and error messages + Pick the roles who have access to this page + Set the login and password for this page + Single user protection + If you just want to setup simple protection using a single login and password + + + + - - + + - + + + - + + + - + + + - Include unpublished subpages - Publishing in progress - please wait... - %0% out of %1% pages have been published... - %0% has been published - %0% and subpages have been published - Publish %0% and all its subpages - Publish to publish %0% and thereby making its content publicly available.

+ ]]> +
+ Include unpublished subpages + Publishing in progress - please wait... + %0% out of %1% pages have been published... + %0% has been published + %0% and subpages have been published + Publish %0% and all its subpages + + Publish to publish %0% and thereby making its content publicly available.

You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]>
- - - You have not configured any approved colors - - - enter external link - choose internal page - Caption - Link - Open in new window - enter the display caption - Enter the link - - - Reset - - - Current version - Red text will not be shown in the selected version. , green means added]]> - Document has been rolled back - This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view - Rollback to - Select version - View - - - Edit script file - - - Concierge - Content - Courier - Developer - Umbraco Configuration Wizard - Media - Members - Newsletters - Settings - Statistics - Translation - Users - Help - Forms - Analytics - - - go to - Help topics for - Video chapters for - The best Umbraco video tutorials - - - Default template - Dictionary Key - To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) - New Tab Title - Node type - Type - Stylesheet - Script - Stylesheet property - Tab - Tab Title - Tabs - Master Content Type enabled - This Content Type uses - as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself - No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. - Master Document Type - Create matching template - Add icon - - - Sort order - Creation date - Sorting complete. - Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items -
Do not close this window during sorting]]>
- - - Validation - Validation errors must be fixed before the item can be saved - Failed - Insufficient user permissions, could not complete the operation - Cancelled - Operation was cancelled by a 3rd party add-in - Publishing was cancelled by a 3rd party add-in - Property type already exists - Property type created - DataType: %1%]]> - Propertytype deleted - Document Type saved - Tab created - Tab deleted - Tab with id: %0% deleted - Stylesheet not saved - Stylesheet saved - Stylesheet saved without any errors - Datatype saved - Dictionary item saved - Publishing failed because the parent page isn't published - Content published - and visible on the website - Content saved - Remember to publish to make changes visible - Sent For Approval - Changes have been sent for approval - Media saved - Media saved without any errors - Member saved - Stylesheet Property Saved - Stylesheet saved - Template saved - Error saving user (check log) - User Saved - User type saved - File not saved - file could not be saved. Please check file permissions - File saved - File saved without any errors - Language saved - Media Type saved - Member Type saved - Python script not saved - Python script could not be saved due to error - Python script saved - No errors in python script - Template not saved - Please make sure that you do not have 2 templates with the same alias - Template saved - Template saved without any errors! - XSLT not saved - XSLT contained an error - XSLT could not be saved, check file permissions - XSLT saved - No errors in XSLT - Content unpublished - Partial view saved - Partial view saved without any errors! - Partial view not saved - An error occurred saving the file. - Script view saved - Script view saved without any errors! - Script view not saved - An error occurred saving the file. - An error occurred saving the file. - - - Uses CSS syntax ex: h1, .redHeader, .blueTex - Edit stylesheet - Edit stylesheet property - Name to identify the style property in the rich text editor - Preview - Styles - - - Edit template - - Sections - Insert content area - Insert content area placeholder - - Insert - Choose what to insert into your template - - Dictionary item - A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. - - Macro - - A Macro is a configurable component which is great for - reusable parts of your design, where you need the option to provide parameters, - such as galleries, forms and lists. - - - Value - Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. - - Partial view - - A partial view is a separate template file which can be rendered inside another - template, it's great for reusing markup or for separating complex templates into separate files. - - - Master template - No master template - No master - - Render child template - - + + + + You have not configured any approved colors + + + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Deleted item + + + enter external link + choose internal page + Caption + Link + Open in new window + enter the display caption + Enter the link + + + Reset + Define crop + Give the crop an alias and its default width and height + Save crop + Add new crop + + + Current version + Red text will not be shown in the selected version. , green means added]]> + Document has been rolled back + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view + Rollback to + Select version + View + + + Edit script file + + + Concierge + Content + Courier + Developer + Umbraco Configuration Wizard + Media + Members + Newsletters + Settings + Statistics + Translation + Users + Help + Forms + Analytics + + + go to + Help topics for + Video chapters for + The best Umbraco video tutorials + + + Default template + Dictionary Key + To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + New Tab Title + Node type + Type + Stylesheet + Script + Stylesheet property + Tab + Tab Title + Tabs + Master Content Type enabled + This Content Type uses + as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself + No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Master Document Type + Create matching template + Add icon + + + Sort order + Creation date + Sorting complete. + Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items + + + + Validation + Validation errors must be fixed before the item can be saved + Failed + Saved + Insufficient user permissions, could not complete the operation + Cancelled + Operation was cancelled by a 3rd party add-in + Publishing was cancelled by a 3rd party add-in + Property type already exists + Property type created + DataType: %1%]]> + Propertytype deleted + Document Type saved + Tab created + Tab deleted + Tab with id: %0% deleted + Stylesheet not saved + Stylesheet saved + Stylesheet saved without any errors + Datatype saved + Dictionary item saved + Publishing failed because the parent page isn't published + Content published + and visible on the website + Content saved + Remember to publish to make changes visible + Sent For Approval + Changes have been sent for approval + Media saved + Media saved without any errors + Member saved + Stylesheet Property Saved + Stylesheet saved + Template saved + Error saving user (check log) + User Saved + User type saved + User group saved + File not saved + file could not be saved. Please check file permissions + File saved + File saved without any errors + Language saved + Media Type saved + Member Type saved + Python script not saved + Python script could not be saved due to error + Python script saved + No errors in python script + Template not saved + Please make sure that you do not have 2 templates with the same alias + Template saved + Template saved without any errors! + XSLT not saved + XSLT contained an error + XSLT could not be saved, check file permissions + XSLT saved + No errors in XSLT + Content unpublished + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. + Permissions saved for + Script view saved + Script view saved without any errors! + Script view not saved + An error occurred saving the file. + An error occurred saving the file. + Deleted %0% user groups + %0% was deleted + Enabled %0% users + An error occurred while enabling the users + Disabled %0% users + An error occurred while disabling the users + %0% is now enabled + An error occurred while enabling the user + %0% is now disabled + An error occurred while disabling the user + User groups have been set + Deleted %0% user groups + %0% was deleted + Unlocked %0% users + An error occurred while unlocking the users + %0% is now unlocked + An error occurred while unlocking the user + + + Uses CSS syntax ex: h1, .redHeader, .blueTex + Edit stylesheet + Edit stylesheet property + Name to identify the style property in the rich text editor + Preview + Styles + + + Edit template + + Sections + Insert content area + Insert content area placeholder + + Insert + Choose what to insert into your template + + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + + Master template + No master template + No master + + Render child template + + @RenderBody() placeholder. ]]> - +
- Define a named section - - Define a named section + + @section { ... }. This can be rendered in a specific area of the parent of this template, by using @RenderSection. ]]> - +
- Render a named section - - Render a named section + + @RenderSection(name) placeholder. This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. ]]> - - - Section Name - Section is mandatory - - If mandatory, the child template must contain a @section definition, otherwise an error is shown. - - - - Query builder - items returned, in - - I want - all content - content of type "%0%" - from - my website - where - and - - is - is not - before - before (including selected date) - after - after (including selected date) - equals - does not equal - contains - does not contain - greater than - greater than or equal to - less than - less than or equal to - - Id - Name - Created Date - Last Updated Date - - order by - ascending - descending - - Template - - - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied - - This content is not allowed here - This content is allowed here - - Click to embed - Click to insert image - Image caption... - Write here... - - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells - - Columns - Total combined number of columns in the grid layout - - Settings - Configure what settings editors can change - - - Styles - Configure what styling editors can change - - Settings will only save if the entered json configuration is valid - - Allow all editors - Allow all row configurations - Set as default - Choose extra - Choose default - are added - - - - Compositions - You have not added any tabs - Add new tab - Add another tab - Inherited from - Add property - Required label - - Enable list view - Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree - - Allowed Templates - Choose which templates editors are allowed to use on content of this type - Allow as root - Allow editors to create content of this type in the root of the content tree - Yes - allow content of this type in the root - - Allowed child node types - Allow content of the specified types to be created underneath content of this type - - Choose child node - Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. - This content type is used in a composition, and therefore cannot be composed itself. - There are no content types available to use as a composition. - - Available editors - Reuse - Editor settings - - Configuration - - Yes, delete - - was moved underneath - was copied underneath - Select the folder to move - Select the folder to copy - to in the tree structure below - - All Document types - All Documents - All media items - - using this document type will be deleted permanently, please confirm you want to delete these as well. - using this media type will be deleted permanently, please confirm you want to delete these as well. - using this member type will be deleted permanently, please confirm you want to delete these as well - - and all documents using this type - and all media items using this type - and all members using this type - - using this editor will get updated with the new settings - - Member can edit - Show on member profile - tab has no sort order - - - - Building models - this can take a bit of time, don't worry - Models generated - Models could not be generated - Models generation has failed, see exception in U log - - - - Alternative field - Alternative Text - Casing - Encoding - Choose field - Convert line breaks - Replaces line breaks with html-tag &lt;br&gt; - Custom Fields - Yes, Date only - Format as date - HTML encode - Will replace special characters by their HTML equivalent. - Will be inserted after the field value - Will be inserted before the field value - Lowercase - None - Insert after field - Insert before field - Recursive - Remove Paragraph tags - Will remove any &lt;P&gt; in the beginning and end of the text - Standard Fields - Uppercase - URL encode - Will format special characters in URLs - Will only be used when the field values above are empty - This field will only be used if the primary field is empty - Yes, with time. Separator: - - - Tasks assigned to you - assigned to you. To see a detailed view including comments, click on "Details" or just the page name. + + + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + + + Query builder + Build a query + items returned, in + + I want + all content + content of type "%0%" + from + my website + where + and + + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + + Id + Name + Created Date + Last Updated Date + + order by + ascending + descending + + Template + + + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + Maximum items + Leave blank or set to 0 for unlimited + Set as default + Choose extra + Choose default + are added + + + + Compositions + You have not added any tabs + Add new tab + Add another tab + Inherited from + Add property + Required label + + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + + Allowed Templates + Choose which templates editors are allowed to use on content of this type + Allow as root + Allow editors to create content of this type in the root of the content tree + Yes - allow content of this type in the root + + Allowed child node types + Allow content of the specified types to be created underneath content of this type + + Choose child node + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + + Available editors + Reuse + Editor settings + + Configuration + + Yes, delete + + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + + All Document types + All Documents + All media items + + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + + and all documents using this type + and all media items using this type + and all members using this type + + using this editor will get updated with the new settings + + Member can edit + Show on member profile + tab has no sort order + + + + Building models + this can take a bit of time, don't worry + Models generated + Models could not be generated + Models generation has failed, see exception in U log + + + + Add fallback field + Fallback field + Add default value + Default value + Fallback field + Default value + Casing + Encoding + Choose field + Convert line breaks + Yes, convert line breaks + Replaces line breaks with 'br' html tag + Custom Fields + Date only + Format and encoding + Format as date + Format the value as a date, or a date with time, according to the active culture + HTML encode + Will replace special characters by their HTML equivalent. + Will be inserted after the field value + Will be inserted before the field value + Lowercase + Modify output + None + Output sample + Insert after field + Insert before field + Recursive + Yes, make it recursive + Separator + Standard Fields + Uppercase + URL encode + Will format special characters in URLs + Will only be used when the field values above are empty + This field will only be used if the primary field is empty + Date and time + + + Tasks assigned to you + + assigned to you. To see a detailed view including comments, click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link.
To close a translation task, please go to the Details view and click the "Close" button. - ]]>
- close task - Translation details - Download all translation tasks as XML - Download XML - Download XML DTD - Fields - Include subpages - + + close task + Translation details + Download all translation tasks as XML + Download XML + Download XML DTD + Fields + Include subpages + + - [%0%] Translation task for %1% - No translator users found. Please create a translator user before you start sending content to translation - Tasks created by you - created by you. To see a detailed view including comments, + ]]> + + [%0%] Translation task for %1% + No translator users found. Please create a translator user before you start sending content to translation + Tasks created by you + + created by you. To see a detailed view including comments, click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link. To close a translation task, please go to the Details view and click the "Close" button. - ]]> - The page '%0%' has been send to translation - Please select the language that the content should be translated into - Send the page '%0%' to translation - Assigned by - Task opened - Total words - Translate to - Translation completed. - You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. - Translation failed, the XML file might be corrupt - Translation options - Translator - Upload translation XML - - - Cache Browser - Recycle Bin - Created packages - Data Types - Dictionary - Installed packages - Install skin - Install starter kit - Languages - Install local package - Macros - Media Types - Members - Member Groups - Member Roles - Member Types - Document Types - Relation Types - Packages - Packages - Python Files - Install from repository - Install Runway - Runway modules - Scripting Files - Scripts - Stylesheets - Templates - XSLT Files - Analytics - - - New update ready - %0% is ready, click here for download - No connection to server - Error checking for update. Please review trace-stack for further information - - - Administrator - Category field - Change Your Password - New password - Confirm new password - You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button - Content Channel - Description field - Disable User - Document Type - Editor - Excerpt field - Language - Login - Start Node in Media Library - Sections - Disable Umbraco Access - Old password - Password - Reset password - Your password has been changed! - Please confirm the new password - Enter your new password - Your new password cannot be blank! - Current password - Invalid current password - There was a difference between the new password and the confirmed password. Please try again! - The confirmed password doesn't match the new password! - Replace child node permissions - You are currently modifying permissions for the pages: - Select pages to modify their permissions - Search all children - Start Node in Content - Name - User permissions - User type - User types - Writer - Translator - Change - Your profile - Your recent history - Session expires in - - - Validation - Validate as email - Validate as a number - Validate as a Url - ...or enter a custom validation - Field is mandatory - - - +
+ + + + + + + + + + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ You have been invited by %1% to the Umbraco Back Office. +

+

+ Message from %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Click this link to accept the invite + +
+
+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %3% + +
+

+
+
+


+
+
+ + ]]> +
+ + + Validation + Validate as email + Validate as a number + Validate as a Url + ...or enter a custom validation + Field is mandatory + Enter a regular expression + You need to add at least + You can only have + items + items selected + Invalid date + Not a number + Invalid email + + + - Value is set to the recommended value: '%0%'. - Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. - Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. - Found unexpected value '%0%' for '%2%' in configuration file '%3%'. + Value is set to the recommended value: '%0%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. + Found unexpected value '%0%' for '%2%' in configuration file '%3%'. - - Custom errors are set to '%0%'. - Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. - Custom errors successfully set to '%0%'. + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. - MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. - MacroErrors are now set to '%0%'. + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. - - Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. - Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). - Try Skip IIS Custom Errors successfully set to '%0%'. - - - File does not exist: '%0%'. - '%0%' in config file '%1%'.]]> - There was an error, check log for full error: %0%. - - Members - Total XML: %0%, Total: %1%, Total invalid: %2% - Media - Total XML: %0%, Total: %1%, Total invalid: %2% - Content - Total XML: %0%, Total published: %1%, Total invalid: %2% - - Your site certificate was marked as valid. - Certificate validation error: '%0%' - Error pinging the URL %0% - '%1%' - You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0% - - - Enable HTTPS - Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure. - - Fix - Cannot fix a check with a value comparison type of 'ShouldNotEqual'. - Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. - Value to fix check not provided. - - Debug compilation mode is disabled. - Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. - Debug compilation mode successfully disabled. - - Trace mode is disabled. - Trace mode is currently enabled. It is recommended to disable this setting before go live. - Trace mode successfully disabled. - - All folders have the correct permissions set. - + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + + Members - Total XML: %0%, Total: %1%, Total invalid: %2% + Media - Total XML: %0%, Total: %1%, Total invalid: %2% + Content - Total XML: %0%, Total published: %1%, Total invalid: %2% + + Your website's certificate is valid. + Certificate validation error: '%0%' + Your website's SSL certificate has expired. + Your website's SSL certificate is expiring in %0% days. + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0% + + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure. + + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. + + All folders have the correct permissions set. + - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> - All files have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> - Set Header in Config - Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. - A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. - Could not update web.config file. Error: %0% + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + Set Header in Config + Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. + A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. + Could not update web.config file. Error: %0% - - %0%.]]> - No headers revealing information about the website technology were found. - - In the Web.config file, system.net/mailsettings could not be found. - In the Web.config file system.net/mailsettings section, the host is not configured. - SMTP settings are configured correctly and the service is operating as expected. - The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. - - %0%.]]> - %0%.]]> - - - Disable URL tracker - Enable URL tracker - Original URL - Redirected To - No redirects have been made - When a published page gets renamed or moved a redirect will automatically be made to the new page. - Remove - Are you sure you want to remove the redirect from '%0%' to '%1%'? - Redirect URL removed. - Error removing redirect URL. - Are you sure you want to disable the URL tracker? - URL tracker has now been disabled. - Error disabling the URL tracker, more information can be found in your log file. - URL tracker has now been enabled. - Error enabling the URL tracker, more information can be found in your log file. - - - No Dictionary items to choose from - -
+ %0%.]]> + No headers revealing information about the website technology were found. + + In the Web.config file, system.net/mailsettings could not be found. + In the Web.config file system.net/mailsettings section, the host is not configured. + SMTP settings are configured correctly and the service is operating as expected. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + + %0%.]]> + %0%.]]> +

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
+ Umbraco Health Check Status + + + Disable URL tracker + Enable URL tracker + Original URL + Redirected To + No redirects have been made + When a published page gets renamed or moved a redirect will automatically be made to the new page. + Remove + Are you sure you want to remove the redirect from '%0%' to '%1%'? + Redirect URL removed. + Error removing redirect URL. + Are you sure you want to disable the URL tracker? + URL tracker has now been disabled. + Error disabling the URL tracker, more information can be found in your log file. + URL tracker has now been enabled. + Error enabling the URL tracker, more information can be found in your log file. + + + No Dictionary items to choose from + + + characters left + + \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/es.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/es.xml index 2de36d8e..5f36f731 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/es.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/es.xml @@ -32,10 +32,8 @@ Enviar a Publicar Enviar a Traducir Ordernar - Enviar a publicación Traducir Actualizar - Valor por defecto Permiso denegado. @@ -220,6 +218,8 @@ Copied %0% out of %1% items + Título del vínculo + Vínculo Nombre Administrar dominios Cerrar esta ventana @@ -472,7 +472,6 @@ El usuario por defecto ha sido desabilitado o ha perdido el acceso a Umbraco!

Pinche en Próximo para continuar.]]> ¡La contraseña del usuario por defecto ha sido cambiada desde que se instaló!

No hay que realizar ninguna tarea más. Pulsa Siguiente para proseguir.]]> ¡La constraseña se ha cambiado! - Umbraco crea un usuario por defecto con un nombre de usuario ('admin') y constraseña ('default'). Es importante que la contraseña se cambie a algo único.

Este paso comprobará la contraseña del usuario por defecto y sugerirá si debe cambiarse.

]]>
Ten un buen comienzo, visita nuestros videos de introducción Pulsando el botón de Siguiente (o modificando el UmbracoConfigurationStatus en el web.config), aceptar la licencia de este software tal y como se especifica en el cuadro de debajo. Ten en cuenta que esta distribución de Umbraco consta de dos licencias diferentes, la licencia open source MIT para el framework y la licencia Umbraco freeware que cubre la IU. No ha sido instalado. @@ -686,8 +685,7 @@ Creation date Ordenación completa. Arrastra las diferentes páginas debajo para colocarlas como deberían estar. O haz click en las cabeceras de las columnas para ordenar todas las páginas - -
No cierre esta ventana mientras se está ordenando ]]>
+ La publicación fue cancelada por un complemento de terceros @@ -790,6 +788,9 @@ Permitir todos los controles de edición Permitir todas las configuraciones de fila + Artículos máximos + Laat dit leeg of is ingesteld op -1 voor onbeperkt + Dejar en blanco o se establece en 0 para ilimitada Campo opcional @@ -809,8 +810,6 @@ Insertar después del campo Insertar antes del campo Recursivo - Borrar los tags del párrafo - Borrará cualquier &lt;P&gt; al principio y al final del texto Mayúscula Codificar URL Formateará los caracteres especiales de las URLs @@ -927,8 +926,6 @@ Nodo de comienzo en contenido Nombre de usuario Permisos de usuarios - Tipo de usuario - Tipos de usuarios Redactor Tu perfil Tu historial reciente diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/fr.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/fr.xml index ec50970e..2587c489 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/fr.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/fr.xml @@ -28,15 +28,16 @@ Rafraîchir Republier le site tout entier Récupérer + Spécifiez les permissions pour la page %0% + Choisissez où déplacer + dans l'arborescence ci-dessous Permissions Version antérieure Envoyer pour publication Envoyer pour traduction Trier - Envoyer pour publication Traduire Mettre à jour - Valeur par défaut Permission refusée. @@ -45,16 +46,16 @@ Noeud invalide. Domaine invalide. Domaine déjà assigné. - Domaine Langue + Domaine Nouveau domaine '%0%' créé Domaine '%0%' supprimé Domaine '%0%' déjà assigné + Domaine '%0%' mis à jour + Editer les domaines actuels
Les domaines contenant un chemin d'un niveau sont autorisés, ex : "example.com/en". Pour autant, cela devrait être évité. Utilisez plutôt la gestion des noms d'hôte.]]>
- Domaine '%0%' mis à jour - Editer les domaines actuels Hériter Culture ou hériter de la culture des noeuds parents. S'appliquera aussi
@@ -85,8 +86,8 @@ Liste numérique Insérer une macro Insérer une image - Retourner à la liste Editer les relations + Retourner à la liste Sauver Sauver et publier Sauver et envoyer pour approbation @@ -97,7 +98,8 @@ Afficher les styles Insérer un tableau Générer les modèles - Sauver et générer les modèles + Défaire + Refaire Pour changer le type de document du contenu séléctionné, faites d'abord un choix dans la liste des types valides à cet endroit. @@ -173,14 +175,15 @@ Cible Ceci se traduit par l'heure suivante sur le serveur : Qu'est-ce que cela signifie?]]> - + Ajouter un autre champ texte + Enlever ce champ texte + Cliquez pour télécharger Faites glisser vos fichier ici... Lien vers le média ou cliquez ici pour choisir un fichier Les seuls types de fichiers autorisés sont - Impossible de télécharger ce fichier, il n'a pas un type de fichier autorisé. La taille maximum de fichier est @@ -196,6 +199,13 @@ Type de document sans modèle Nouveau répertoire Nouveau type de données + Nouveau fichier javascript + Nouvelle vue partielle vide + Nouvelle macro pour vue partielle + Nouvelle vue partielle à partir d'un snippet + Nouvelle macro pour vue partielle vide + Nouvelle macro pour vue partielle à partir d'un snippet + Nouvelle macro pour vue partielle (sans macro) Parcourir votre site @@ -241,6 +251,8 @@ %0% éléments sur %1% copiés + Titre du lien + Lien Nom Gérer les noms d'hôtes Fermer cette fenêtre @@ -289,10 +301,12 @@ Voir l'élément de cache Créer un répertoire... Lier à l'original + Inclure les descendants La communauté la plus amicale Lier à la page Ouvre le document lié dans une nouvelle fenêtre ou un nouvel onglet Lier à un media + Lier à un fichier Sélectionner le media Sélectionner l'icône Sélectionner l'élément @@ -301,7 +315,9 @@ Sélectionner le contenu Sélectionner le membre Sélectionner le groupe de membres + Aucune icone n'a été trouvée Il n'y a pas de paramètres pour cette macro + Il n'y a pas de macro disponible à insérer Fournisseurs externes d'identification Détails de l'exception Trace d'exécution @@ -310,12 +326,19 @@ Enlevez votre compte Sélectionner un éditeur + Selectionner un snippet %0%' ci-dessous.
Vous pouvez ajouter d'autres langues depuis le menu ci-dessous "Langues". ]]>
Nom de Culture + Modifiez la clé de l'élément de dictionaire. + + + Votre nom d'utilisateur @@ -331,7 +354,6 @@ Entrez votre email Votre nom d'utilisateur est généralement votre adresse email - Autoriser à la racine Seuls les Types de Contenu qui ont ceci coché peuvent être créés au niveau racine des arborescences de contenu et de media @@ -362,6 +384,13 @@ CSS associées Afficher le libellé Largeur et hauteur + Tous les types de propriétés & les données de propriétés + utilisant ce type de données seront supprimés définitivement, veuillez confirmer que vous voulez également les supprimer + Oui, supprimer + et tous les types de propriétés & les données de propriétés utilisant ce type de données + Sélectionnez le répertoire où déplacer + dans l'arborescence ci-dessous + a été déplacé sous Vos données ont été sauvegardées, mais avant de pouvoir publier votre page, il y a des erreurs que vous devez corriger : @@ -420,6 +449,7 @@ Fermer la fenêtre Commenter Confirmer + Conserver Conserver les proportions Continuer Copier @@ -431,6 +461,7 @@ Supprimé Suppression... Design + Dictionnaire Dimensions Bas Télécharger @@ -440,6 +471,7 @@ Email Erreur Trouver + Premier Hauteur Aide Icône @@ -449,8 +481,8 @@ Installer Non valide Justifier - Libellé Langue + Dernier Mise en page En cours de chargement Bloqué @@ -477,8 +509,8 @@ Propriétés Email de réception des données de formulaire Corbeille - Votre corbeille est vide Restant + Enlever Renommer Renouveller Requis @@ -486,6 +518,7 @@ Permissions Rechercher Désolé, nous ne pouvons pas trouver ce que vous recherchez + Aucun élément n'a été ajouté Serveur Montrer Afficher la page à l'envoi @@ -519,6 +552,7 @@ Intégrer sélectionné + Noir Vert @@ -527,6 +561,7 @@ Bleu Rouge + Ajouter un onglet Ajouter une propriété @@ -544,7 +579,18 @@ Passer à la vue en liste Basculer vers l'autorisation comme racine + + Commenter/Décommenter les lignes + Supprimer la ligne + Copier les lignes vers le haut + Copier les lignes vers le bas + Déplacer les lignes vers le haut + Déplacer les lignes vers le bas + + Général + Editeur + Couleur de fond Gras @@ -552,6 +598,7 @@ Police Texte + Page @@ -580,18 +627,13 @@ N'ayez pas d'inquiétude : aucun contenu ne sera supprimé et tout continuera à fonctionner parfaitement par après !

]]>
- - Appuyez sur Suivant pour - poursuivre. ]]> - + Appuyez sur Suivant pour + poursuivre. ]]> Suivant pour poursuivre la configuration]]> Le mot de passe par défaut doit être modifié !]]> L'utilisateur par défaut a été désactivé ou n'a pas accès à Umbraco!

Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> Le mot de passe par défaut a été modifié avec succès depuis l'installation!

Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> Le mot de passe a été modifié ! - - ('admin') et le mot de passe ('default'). Il est important que ce mot de passe soit modifié en quelque-chose de sécurisé et unique. - ]]> Pour bien commencer, regardez nos vidéos d'introduction En cliquant sur le bouton "Suivant" (ou en modifiant umbracoConfigurationStatus dans le fichier web.config), vous acceptez la licence de ce logiciel telle que spécifiée dans le champ ci-dessous. Veuillez noter que cette distribution Umbraco consiste en deux licences différentes, la licence open source MIT pour le framework et la licence Umbraco freeware qui couvre l'UI. Pas encore installé. @@ -619,8 +661,7 @@ Il stocke également des données temporaires (i.e : cache) pour améliorer les performances de votre site. ]]> Je veux démarrer "from scratch" - - Apprenez comment) Vous pouvez toujours choisir d'installer Runway plus tard. Pour cela, allez dans la section "Développeur" et sélectionnez "Packages". @@ -633,8 +674,7 @@ ]]> Recommandé uniquement pour les utilisateurs expérimentés Je veux commencer avec un site simple - - "Runway" est un site simple qui fournit des types de documents et des modèles de base. L'installateur peut mettre en place Runway automatiquement pour vous, mais vous pouvez facilement l'éditer, l'enrichir, ou le supprimer par la suite. Il n'est pas nécessaire, et vous pouvez parfaitement vous en passer pour utiliser Umbraco. Cela étant dit, @@ -738,8 +778,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Avec les salutations du Robot Umbraco ]]> - - Hello %0%

+ Hello %0%

Ceci est un email automatique pour vous informer que la tâche '%1%' a été executée sur la page '%2%' @@ -774,6 +813,39 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Choisissez un package sur votre ordinateur en cliquant sur le bouton Parcourir
et localisez le package. Les packages Umbraco ont généralement une extension ".umb" ou ".zip". ]]> + Déposez pour uploader + ou cliquez ici pour choisir les fichiers + Uploader un package + Installez un package local en le sélectionnant sur votre ordinateur. Installez uniquement des packages de sources fiables que vous connaissez + Uploader un autre package + Annuler et uploader un autre package + Licence + J'accepte + les conditions d'utilisation + Installer le package + Terminer + Packages installés + Vous n'avez aucun package installé + 'Packages' en haut à droite de votre écran]]> + Chercher des packages + Résultats pour + Nous n'avons rien pu trouver pour + Veuillez essayer de chercher un autre package ou naviguez à travers les catégories + Populaires + Nouvelles releases + a + points de karma + Information + Propriétaire + Contributeurs + Créé + Version actuelle + version .NET + Téléchargements + Coups de coeur + Compatibilité + Ce package est compatible avec les versions suivantes de Umbraco, selon les rapports des membres de la communauté. Une compatibilité complète ne peut pas être garantie pour les versions rapportées sous 100% + Sources externes Auteur Démo Documentation @@ -808,6 +880,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Installation... Redémarrage, veuillez patienter... Terminé, votre navigateur va être rafraîchi, veuillez patienter... + Veuillez cliquer sur terminer pour compléter l'installation et recharger la page. + Package en cours de chargement... Coller en conservant le formatage (non recommandé) @@ -875,6 +949,10 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Réinitialiser + Définir le recadrage + Donnez un alias au recadrage ainsi que sa largeur et sa hauteur par défaut + Sauvegarder le recadrage + Ajouter un nouveau recadrage Version actuelle @@ -937,7 +1015,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Date de création Tri achevé. Faites glisser les différents éléments vers le haut ou vers le bas pour définir la manière dont ils doivent être organisés. Ou cliquez sur les entêtes de colonnes pour trier la collection complète d'éléments -
Ne fermez pas cette fenêtre durant le tri.]]>
+ Validation @@ -1015,17 +1093,113 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Prévisualiser Styles + Editer le modèle + + Sections Insérer une zone de contenu Insérer un placeholder de zone de contenu - Insérer un élément de dictionnaire - Insérer une Macro - Insérer un champ de la page Umbraco + + Insérer + Choisissez l'élément à insérer dans votre modèle + + Elément de dictionnaire + Un élément de dictionnaire est un espace pour un morceau de texte traduisible, ce qui facilite la création de designs pour des sites web multilangues. + + Macro + + Une Macro est un composant configurable, ce qui est génial pour les parties réutilisables de votre + design où vous devez pouvoir fournir des paramètres, + comme les galeries, les formulaires et les listes. + + + Valeur + Affiche la valeur d'un des champs de la page en cours, avec des options pour modifier la valeur ou spécifier des valeurs alternatives. + + Vue partielle + + Une vue partielle est un fichier modèle séparé qui peut être à l'intérieur d'un aute modèle, + c'est génial pour réutiliser du markup ou pour séparer des modèles complexes en plusieurs fichiers. + + Modèle de base - Guide rapide concernant les tags des modèles Umbraco + Pas de modèle de base + Pas de modèle + + Afficher un modèle enfant + + @RenderBody(). + ]]> + + + + Définir une section nommée + + @section { ... }. Celle-ci peut être affichée dans une région + spécifique du parent de ce modèle, en utilisant @RenderSection. + ]]> + + + Afficher une section nommée + + @RenderSection(name). + Ceci affiche une région d'un modèle enfant qui est entourée d'une définition @section [name]{ ... } correspondante. + ]]> + + + Nom de la section + La section est obligatoire + + Si obligatoire, le modèle enfant doit contenir une définition @section, sinon une erreur est affichée. + + + + Générateur de requêtes + Générer une requête + éléments trouvés, en + + Je veux + tout le contenu + le contenu du type "%0%" + à partir de + mon site web + + et + + est + n'est pas + avant + avant (incluant la date sélectionnée) + après + après (incluant la date sélectionnée) + égal + n'est pas égal + contient + ne contient pas + supérieur à + supérieur ou égal à + inférieur à + inférieur ou égal à + + Id + Nom + Date de Création + Date de Dernière Modification + + trier par + ascendant + descendant + Modèle + + Choisissez le type de contenu Choisissez une mise en page @@ -1057,7 +1231,6 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Paramètres Configurez les paramètres qui peuvent être modifiés par les éditeurs - Styles Configurez les effets de style qui peuvent être modifiés par les éditeurs @@ -1070,8 +1243,9 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Choisir le défaut ont été ajoutés - - + + + Compositions Vous n'avez pas ajouté d'onglet Ajouter un nouvel onglet @@ -1085,6 +1259,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Modèles autorisés Sélectionnez les modèles que les éditeurs sont autorisés à utiliser pour du contenu de ce type. + Autorisé comme racine Autorisez les éditeurs à créer du contenu de ce type à la racine de l'arborescence de contenu. Oui - autoriser du contenu de ce type à la racine @@ -1093,6 +1268,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Autorisez la création de contenu des types spécifiés sous le contenu de ce type-ci Choisissez les noeuds enfants + Hériter des onglets et propriétés d'un type de document existant. De nouveaux onglets seront ajoutés au type de document actuel, ou fusionnés s'il existe un onglet avec un nom sililaire. Ce type de contenu est utilisé dans une composition, et ne peut donc pas être lui-même un composé. Il n'y a pas de type de contenu disponible à utiliser dans une composition. @@ -1127,39 +1303,40 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Le membre peut éditer Afficher dans le profil du membre - l'onglet n'a pas d'ordonnancement - - - - Création des modèles - ceci peut prendre un certain temps, ne vous inquiétez pas - Les modèles ont été générés - Les modèles n'ont pas pu être générés - La génération des modèles a échoué, veuillez consulter les erreurs dans le log Umbraco + + Ajouter un champ de rechange + Champ de rechange + Ajouter une valeur par défaut + Valeur par défaut Champ alternatif Texte alternatif Casse Encodage Choisir un champ Convertir les sauts de ligne + Oui, convertir les sauts de ligne Remplace les sauts de ligne avec des balises &lt;br&gt; Champs particuliers Oui, la date seulement + Format et encodage Formater comme une date + Formate la valeur comme une date, ou une date avec l'heure, en fonction de la culture active Encoder en HTML Remplacera les caractères spéciaux par leur équivalent HTML. Sera inséré après la valeur du champ Sera inséré avant la valeur du champ Minuscules + Modifier le résultat Aucun + Example de résultat Insérer après le champ Insérer avant le champ Récursif - Supprimer les balises de paragraphes - Supprimera toute balise &lt;P&gt; au début et à la fin du texte + Oui, rendre récursif + Séparateur Champs standards Majuscules Encode pour URL @@ -1248,6 +1425,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Modèles Fichiers XSLT Analytique + Vues partielles + Fichiers Macro pour les vues partielles Nouvelle mise à jour disponible @@ -1291,8 +1470,6 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Noeud de départ du contenu Nom d'utilisateur Permissions utilisateur - Type d'utilisateur - Types d'utilisateurs Rédacteur Traducteur Modifier @@ -1307,6 +1484,14 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Valider comme Url ...ou introduisez une validation spécifique Champ obligatoire + Introduisez une expression régulière + Vous devez ajouter au moins + Vous ne pouvez avoir que + éléments + éléments sélectionnés + Date non valide + Pas un nombre + Email non valide + Send Type Søk... Opp @@ -441,6 +439,17 @@ Ja Mappe Søkeresultater + Sorter + Avslutt sortering + Eksempel + Bytt passord + til + Listevisning + Lagrer... + nåværende + Innbygging + Hent + valgt Bakgrunnsfarge @@ -468,7 +477,6 @@ Standardbrukeren har blitt deaktivert eller har ingen tilgang til Umbraco!

Ingen videre handling er nødvendig. Klikk neste for å fortsette.]]> Passordet til standardbrukeren har blitt forandret etter installasjonen!

Ingen videre handling er nødvendig. Klikk Neste for å fortsette.]]> Passordet er blitt endret! - Umbraco skaper en standard bruker med login ( "admin") og passord ( "default") . Det er viktig at passordet er endret til noe unikt.

Dette trinnet vil sjekke standard brukerens passord og foreslår hvis det må skiftes ]]> Få en god start med våre introduksjonsvideoer Ved å klikke på Neste-knappen (eller endre UmbracoConfigurationStatus i Web.config), godtar du lisensen for denne programvaren som angitt i boksen nedenfor. Legg merke til at denne Umbraco distribusjon består av to ulike lisenser, åpen kilde MIT lisens for rammen og Umbraco frivareverktøy lisens som dekker brukergrensesnittet. Ikke installert. @@ -736,7 +744,7 @@ Vennlig hilsen Umbraco roboten Creation date Sortering ferdig. Dra elementene opp eller ned for å arrangere dem. Du kan også klikke kolonneoverskriftene for å sortere alt på en gang. -
Ikke lukk dette vinduet under sortering]]>
+ En feil oppsto @@ -884,8 +892,6 @@ Vennlig hilsen Umbraco roboten Sett inn etter felt Sett inn før felt Rekursivt - Fjern paragraftagger - Fjerner eventuelle <P> rundt teksten Standardfelter Store bokstaver URL koding @@ -1005,8 +1011,6 @@ Vennlig hilsen Umbraco roboten Startnode Navn Brukertillatelser - Brukertype - Brukertyper Forfatter Oversetter Endre diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/nl.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/nl.xml index a0f98c99..872be541 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/nl.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/nl.xml @@ -27,19 +27,15 @@ Depubliceren Nodes opnieuw inladen Herpubliceer de site - Stel rechten voor pagina %0% in - Waarheen verplaatsen/ - Naar de onderstaande boomstructuur - Herstellen + Stel rechten voor pagina %0% in + Herstellen Rechten Vorige versies Klaar voor publicatie Klaar voor vertalen Sorteren - Klaar voor publicatie Vertalen Bijwerken - Standaardwaarde Permission denied. @@ -172,7 +168,7 @@ Link naar het document Lid van groep(en) Geen lid van groep(en) - Kinderen + Subitems Doel Dit betekend de volgende tijd op de server: Wat houd dit in?]]> @@ -190,6 +186,10 @@ Maak nieuwe member aan Alle Members + + Nieuw lid aanmaken + Alle Leden + Waar wil je de nieuwe %0% aanmaken? Aanmaken onder @@ -244,6 +244,8 @@ item %0% van de %1% gekopieerd + Link Titel + Link Naam Beheer domeinnamen Sluit dit venster @@ -581,7 +583,6 @@ De default gebruiker is geblokkeerd of heeft geen toegang tot Umbraco!

Geen verdere actie noodzakelijk. Klik Volgende om verder te gaan.]]> Het wachtwoord van de default gebruiker is sinds installatie met succes veranderd.

Geen verdere actie noodzakelijk. Klik Volgende om verder te gaan.]]> Het wachtwoord is veranderd! - Umbraco maakt een default gebruiker aan met login ('admin') and wachtwoord ('default'). Het is belangrijk dat dit wachtwoord wordt veranderd in iets unieks.

Deze stap controleert het password van de default gebruiker en adviseert of het veranderd dient te worden.

]]>
Neem een jumpstart en bekijk onze introductie videos De huidige node is niet toegestaan onder de geselecteerde node vanwege het node type De huidige node kan niet naar een van zijn subpagina’s worden verplaatst. De huidige node kan niet worden gebruikt op root-niveau - Deze actie is niet toegestaan omdat je onvoldoende rechten hebt op 1 of meer kinderen. + Deze actie is niet toegestaan omdat je onvoldoende rechten hebt op 1 of meer subitems. Relateer gekopieerde items aan het origineel @@ -827,7 +828,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Plakken, en verwijder de opmaak (aanbevolen) - Geavanceerd: Beveilig door de Member Groups te seecteren die toegang hebben op de pagina + Geavanceerd: Beveilig door de Member Groups te selecteren die toegang hebben op de pagina gebruik makend van Umbraco's member groups.]]> Je moet eerst een membergroup maken voordat je kunt werken met role-based authentication. Error Pagina @@ -862,14 +863,14 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je - Inclusief ongepubliceerde kinderen + Inclusief ongepubliceerde subitems Publicatie in uitvoering - even geduld... %0% van %1% pagina’s zijn gepubliceerd... %0% is gepubliceerd %0% en onderliggende pagina’s zijn gepubliceerd - Publiceer %0% en alle kinderen + Publiceer %0% en alle subitems ok om %0% te publiceren en de wijzigingen zichtbaar te maken voor bezoekers.

- Je kunt deze pagina publiceren en alle onderliggende sub-pagina's door publiceer alle kinderen aan te vinken hieronder. + Je kunt deze pagina publiceren en alle onderliggende sub-pagina's door publiceer alle subitems aan te vinken hieronder. ]]>
@@ -948,7 +949,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Creation date Sorteren gereed. Sleep de pagina's omhoog of omlaag om de volgorde te veranderen. Of klik op de kolom-header om alle pagina's daarop te sorteren. -
Sluit dit venster niet tijdens het sorteren]]>
+ Validatie @@ -1076,7 +1077,9 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Alle editors toelaten Alle rijconfiguraties toelaten - Instellen als standaard + Maximale artikelen + Laat dit leeg of is ingesteld op -1 voor onbeperkt + Instellen als standaard Kies extra Kies standaard zijn toegevoegd @@ -1169,8 +1172,6 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Invoegen na veld Invoegen voor veld Recursief - Verwijder paragraaf tags - tags aan het begin en einde van de tekst worden verwijderd]]> Standaard velden Hoofdletters URL-encoderen @@ -1295,15 +1296,13 @@ Om een vertalingstaak te sluiten, ga aub naar het detailoverzicht en klik op de Ongeldig huidig wachtwoord Beide wachtwoorden waren niet hetzelfde. Probeer opnieuw! Beide wachtwoorden zijn niet hetzelfde! - Vervang rechten op de subnodes + Vervang rechten op de subitems U bent momenteel rechten aan het aanpassen voor volgende pagina's: Selecteer pagina's om hun rechten aan te passen - Doorzoek alle subnodes + Doorzoek alle subitems Startnode in Content Gebruikersnaam Gebruikersrechten - Gebruikerstype - Gebruikerstypes Auteur Vertaler Wijzig diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/pl.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/pl.xml index a356731f..7f6ea69f 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/pl.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/pl.xml @@ -8,6 +8,7 @@ Zarządzanie hostami Historia zmian Przeglądaj węzeł + Zmień typ dokumentu Kopiuj Utwórz Stwórz zbiór @@ -15,8 +16,6 @@ Deaktywuj Opróżnij kosz Eksportuj typ dokumentu - Ekspo .NET' - Eksportuj do .NET' Importuj typ dokumentu Importuj zbiór Edytuj na stronie @@ -25,31 +24,50 @@ Powiadomienia Publiczny dostęp Opublikuj + Cofnij publikację Odśwież węzeł Opublikuj ponownie całą stronę + Przywróć + Ustaw uprawnienia dla strony %0% Uprawnienia Cofnij Wyślij do publikacji Wyślij do tłumaczenia Sortuj - Zapisz do opublikowania Przetłumacz Aktualizuj + Brak odpowiednich uprawnień Dodaj nową domenę + Usuń domenę + Niepoprawny węzeł + Niepoprawny format domeny. + Domena została już przydzielona. + Język Domena Domena '%0%' została utworzona Domena '%0%' została skasowana Domena '%0%' jest aktualnie przypisana - np.: yourdomain.com, www.yourdomain.com Domena '%0%' została zaktualizowana + Edycja aktualnych domen + Odziedziczona + Język + lub wybierz dziedziczenie języka z węzła rodzica. Zostanie to zastosowane
+także do obecnego węzła, o ile poniższa domena również do niego należy.]]>
+ Domeny Wyświetlane dla + Wyczyść sekcję + Wybierz + Wybierz bieżący folder + Zrób coś innego Pogrubienie Zmniejsz wcięcie Wstaw z pola @@ -67,97 +85,168 @@ Wstawianie makra Wstawianie obrazka Edycja relacji + Powrót do listy Zapisz Zapisz i publikuj - Zapisz i wyślij do sprawdzenia + Zapisz i wyślij do zaakceptowania + Zapisz widok listy Podgląd + Podgląd jest wyłączony, ponieważ żaden szablon nie został przydzielony Wybierz styl Pokaż style Wstaw tabelę + Wygeneruj modele + Cofnij + Powtórz + + + Aby zmienić typ dokumentu dla wybranej treści, najpierw wybierz typ z listy typów obowiązujących dla tej lokalizacji. + Następnie potwierdź i/lub zmień mapowanie właściwości z bieżącego typu do nowego i kliknij "Zapisz". + Treść została opublikowana ponownie. + Bieżąca właściwość + Bieżący typ + Typ dokumentu nie może być zmieniony, ponieważ nie istnieją obowiązujące alternatywy dla tej lokalizacji. Alternatywa będzie dostępna, jeśli będzie dozwolona pod rodzicem wybranego elementu zawartości i jeśli wszystkie istniejące dzieci elementu zawartości będą mieć pozwolenie na bycie tworzonym pod rodzicem. + Typ dokumentu został zmieniony + Mapuj Właściwości + Mapuj do Właściwości + Nowy Szablon + Nowy typ + Nic + Zawartość + Wybierz Nowy Typ Dokumentu + Typ dokumentu wybranej zawartości został zmieniony z powodzeniem do [new type] i następujące właściwości zostały zmapowane: + do + Nie można dokończyć mapowania właściwości, ponieważ jedna lub więcej właściwości mają zdefiniowane więcej niż jedno mapowanie. + Wyświetlane są tylko alternatywne typy obowiązujące dla obecnej lokalizacji. + Jest Opublikowany O tej stronie Link alternatywny (jakbyś opisał obrazek nad telefonem) Alternatywne linki Kliknij, aby edytować ten element Utworzone przez + Pierwotny autor + Zaktualizowane przez Data utworzenia + Data/czas stworzenia tego dokumentu Rodzaj dokumentu Edytowanie - Usuń w + Usuń w Ten element został zmieniony po publikacji Element nie jest opublikowany Opublikowane + Nie ma żadnych elementów do wyświetlenia + Nie ma żadnych elementów do wyświetlenia w liście. Typ mediów + Link do elementu(ów) mediów Członek grupy Rola - Członek typ + Typ członka Brak daty Tytuł strony Właściwości Ten dokument jest opublikowany, ale jest niewidoczny, ponieważ jego rodzic '%0%' nie jest opublikowany + Ten dokument jest opublikowany, ale nie jest w cache. + Nie znaleziono URL + Ten dokument jest opublikowany ale jego URL kolidowałby z elementem treści %0% Publikuj - Status + Status publikacji Opublikuj + Cofnij publikację Data usunięcia Porządek się zmienił - Aby posortować gałęzie, poprostu przeciągnij gałąż lub kliknij na jednym z nagłówków kolumn. Możesz wybrać kilka gałęzi poprzez przytrzymanie klawisza "shift" lub "control" podczas zaznaczania + Aby posortować gałęzie, po prostu przeciągnij gałąż lub kliknij na jednym z nagłówków kolumn. Możesz wybrać kilka gałęzi poprzez przytrzymanie klawisza "shift" lub "control" podczas zaznaczania Statystyki Tytuł (opcjonalny) + Alternatywny tekst (opcjonalny) Typ Cofnij publikację Ostatnio edytowany - Usuń plik + Data/czas edycji dokumentu + Usuń plik(i) Link do dokumentu + Członek grupy (grup) + Nie jest członkiem grupy (grup) + Elementy dzieci + Cel + Oznacza to następującą godzinę na serwerze: + Co to oznacza?]]> + Dodaj kolejne pole tekstowe + Usuń te pole tekstowe + + + Kliknij, aby załadować plik + Przerzuć swoje pliki tutaj... + Link do mediów + lub kliknij tutaj, aby wybrać pliki + Jedyne dozwolone typy plików to + Maksymalny rozmiar pliku to + + + Stwórz nowego członka + Wszyscy członkowie Gdzie chcesz stworzyć nowy %0%? Utwórz w Wybierz rodzaj oraz tytuł + "typy dokumentów"
.]]>
+ "typy mediów".]]> + Typ Dokumentu bez szablonu + Nowy folder + Nowy typ danych + Nowy plik javascript + Nowy pusty Częściowy Widok + Nowy Częściowy Widok makro + Nowy Częściowy Widok ze snippeta + Nowy pusty Częściowy Widok makro + Nowy Częściowy Widok makro ze snippeta + Nowy Częściowy Widok makro (bez makro) Przeglądaj swoją stronę - Ukryj - Jeśli Umbraco się nie otwiera, prawdopodbnie musisz zezwolić tej stronie na otwieranie wyskakujących okienek + Jeśli Umbraco się nie otwiera, prawdopodobnie musisz zezwolić tej stronie na otwieranie wyskakujących okienek zostało otwarte w nowym oknie Restartuj Odwiedź Witaj - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + Zostań + Odrzuć zmiany + Masz niezapisane zmiany + Jesteś pewien, że chcesz wyjść ze strony? - masz niezapisane zmiany - Done + Wykonane - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items + Usunięto %0% element + Usunięto %0% elementy(ów) + Usunięto %0% z %1% elementu + Usunięto %0% z %1% elementów - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items + Opublikowano %0% element + Opublikowano %0% elementy(ów) + Opublikowano %0% z %1% elementu + Opublikowano %0% z %1% elementów - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items + Cofnięto publikację %0% elementu + Cofnięto publikację %0% elementy(ów) + Cofnięto publikację %0% z %1% elementu + Cofnięto publikację %0% z %1% elementów - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items + Przeniesiono %0% element + Przeniesiono %0% elementy(ów) + Przeniesiono %0% z %1% elementu + Przeniesiono %0% z %1% elementów - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items + Skopiowano %0% element + Skopiowano %0% elementy(ów) + Skopiowano %0% z %1% elementu + Skopiowano %0% z %1% elementów Nazwa @@ -183,45 +272,105 @@ Link wewnętrzny: Kiedy używasz odnośników lokalnych, wstaw znak "#" na początku linku Otworzyć w nowym oknie? - Ustawienia Makra + Ustawienia Makro To makro nie posiada żadnych właściwości, które można edytować Wklej Edytuj Uprawnienia dla Zawartość kosza jest teraz usuwana. Proszę nie zamykać tego okna do momentu zakończenia procesu. Zawartość kosza została usunięta Usunięcie elementów z kosza powoduje ich trwałe i nieodwracalne skasowanie - regexlib.com aktulanie nie jest dostępny, na co nie mamy wpływu. Bardzo przepraszamy za te utrudnienia.]]> - Przeszukaj dla wyrażeń regularnych aby dodać regułę sprawdzającą do formularza. Np. 'email' 'url' + regexlib.com aktualnie nie jest dostępny, na co nie mamy wpływu. Bardzo przepraszamy za te utrudnienia.]]> + Przeszukaj dla wyrażeń regularnych, aby dodać regułę sprawdzającą do formularza. Np. 'email' 'url' Usuń Makro Pole wymagane Strona została przeindeksowana Cache strony zostało odświeżone. Cała opublikowana zawartość jest teraz aktualna. Natomiast cała nieopublikowana zawartość ciągle nie jest widoczna - Cache strony zostanie odświeżone. Cała zawartość opublikowana będzie aktulana, lecz nieopublikowana zawartość pozostanie niewidoczna + Cache strony zostanie odświeżone. Cała zawartość opublikowana będzie aktualna, lecz nieopublikowana zawartość pozostanie niewidoczna Liczba kolumn Liczba wierszy - Ustaw zastępczy ID Ustawiając ID na tym elemencie możesz później łączyć treść z podrzędnych szablonów, ustawiając dowiązanie do tego ID na elemencie <asp:treści />]]> - Wybierz zastępczy id z poniższej listy. Możesz wybierać tylko spośród id na szablonie nadrzędnym tego formularza.]]> - Kliknij na obrazie, aby zobaczyć je w pełnym rozmiarze + Ustaw zastępczy ID Ustawiając ID na tym elemencie możesz później łączyć treść z podrzędnych szablonów, + ustawiając dowiązanie do tego ID na elemencie <asp:treści />]]> + Wybierz zastępczy ID z poniższej listy. Możesz wybierać tylko + spośród ID na szablonie nadrzędnym tego formularza.]]> + Kliknij na obrazie, aby zobaczyć go w pełnym rozmiarze Wybierz element Podgląd elementów Cache + Utwórz folder... + Odnieś się do oryginału + Zawrzyj potomków + Najbardziej przyjacielska społeczność + Link do strony + Otwórz zlinkowany dokument w nowym oknie lub zakładce + Link do mediów + Link do plików + Wybierz media + Wybierz ikonę + Wybierz element + Wybierz link + Wybierz makro + Wybierz zawartość + Wybierz członka + Wybierz członka grupy + Nie znaleziono ikon + Te makro nie ma żadnych właściwości + Brak dostępnych makro do wstawienia + Zewnętrzni dostawcy logowania + Sczegóły Wyjątku + Stacktrace + Wewnętrzny wyjątek + Zlinkuj swój + Odlinkuj swój + konto + Wybierz edytora + Wybierz snippet - %0%' poniżej.
+ %0%' poniżej.
Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]>
Nazwa języka + Edytuj klucz elementu słownika. + + + + + + Wpisz nazwę użytkownika + Wpisz hasło + Potwierdź hasło + Nazwij %0%... + Wpisz nazwę... + Etykieta... + Wpisz opis... + Wpisz, aby wyszukać... + Wpisz, aby filtrować... + Wpisz, aby dodać tagi (naciśnij enter po każdym tagu)... + Wpisz adres e-mail + Twoja nazwa użytkownika to przeważnie Twój adres e-mail + Zezwól w korzeniu + Tylko Typ Zawartości, który jest zaznaczony może być stworzony na pozomie korzenia drzew Zawartości i Mediów Dozwolone węzły pochodne + Kompozycje Typu Dokumentu Stwórz Usuń zakładkę Opis Nowa zakładka Zakładka Miniatura + Włącz widok listy + Konfiguruje element treści, aby pokazywał możliwą do sortowania i szukania listę jego dzieci, dzieci nie będą wyświetlone w drzewie + Bieżący widok listy + Typ danych aktywnego widoku listy + Stwórz niestandardowy widok listy + Usuń niestandardowy widok listy Dodaj wartość - Typ bazydanych + Typ bazy danych Edytor GUID Renderuj kontrolkę Przyciski @@ -231,10 +380,17 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]>
Powiązane arkusze stylów Pokaż etykietę Szerokość i wysokość + Wszystkie typy właściwości & dane właściwości + używające tego typu danych zostaną usunięte na zawsze, potwierdź, że chcesz je także usunąć + Tak, usuń + i wszystkie typy właściwości & dane właściwości używające tego typu danych + Wybierz folder do przeniesienia + do w strukturze drzewa poniżej + został przeniesiony poniżej - Dane zostały zapisane, lecz wystąpiły błędy które musisz poprawić przed publikacją strony: - Bieżący dostawca Membership nie obsługuje zmiany hasła (EnablePasswordRetrieval musi mieć wartość true) + Dane zostały zapisane, lecz wystąpiły błędy, które musisz poprawić przed publikacją strony: + Bieżący dostawca członkowstwa nie obsługuje zmiany hasła (EnablePasswordRetrieval musi mieć wartość 'true') %0% już istnieje Wystąpiły błędy: Wystąpiły błędy: @@ -246,30 +402,42 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> %0% nie jest w odpowiednim formacie - TRANSLATE ME: 'NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough.' - Proszę uzupełnij zarówno alias jak i nazwę dla nowego typu właściwości + Otrzymano błąd serwera + Określony typ pliku został ustawiony jako niedozwolony przez administratora + Pomimo tego, że CodeMirror jest włączony w konfiguracji, jest on wyłączony w Internet Explorerze ze względu na swoją niestabilność. + Proszę uzupełnij zarówno alias, jak i nazwę dla nowego typu właściwości Wystąpił problem podczas zapisu/odczytu wymaganego pliku lub folderu + Wystąpił błąd podczas ładowania skryptu Częściowego Widoku (plik: %0%) + Wystąpił błąd podczas ładowania userControl '%0%' + Wystąpił błąd podczas ładowania customControl (Assembly: %0%, Typ: '%1%') + Wystąpił błąd podczas ładowania skryptu MacroEngine (plik: %0%) + "Wystąpił błąd podczas parsowania pliku XSLT: %0% + "Wystąpił błąd odczytu pliku XSLT: %0% Proszę podać tytuł Proszę wybrać typ Chcesz utworzyć obraz większy niż rozmiar oryginalny. Czy na pewno chcesz kontynuować? - Błąd w skrypcie python - Skrypt python nie został zapisany, ponieważ zawiera błędy + Błąd w skrypcie Python + Skrypt Python nie został zapisany, ponieważ zawiera błędy Węzeł początkowy usunięto, proszę skontaktować się z administratorem Proszę zaznaczyć zawartość przed zmianą stylu Brak dostępnych aktywnych stylów - Proszę ustaw kursor po lewej stronie dwóch cel które chcesz połączyć + Proszę ustaw kursor po lewej stronie dwóch komórek, które chcesz połączyć Nie możesz podzielić komórki, która nie była wcześniej połączona. Błąd w źródle XSLT Plik XSLT nie został zapisany, ponieważ wystąpiły błędy + Wystąpił błąd konfiguracji związany z typem danych użytych we właściwościach, proszę sprawdź typ danych O... Akcja + Akcje Dodaj Alias + Wszystkie Czy jesteś pewny? + Wstecz Ramka - lub + przez Anuluj Marginesy komórki Wybierz @@ -277,6 +445,7 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Zamknij okno Komentarz Potwierdzenie + Zachowaj Zachowaj proporcje Kontynuuj Kopiuj @@ -288,6 +457,7 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Usunięto Usuwanie... Wygląd + Słownik Rozmiary Dół Pobierz @@ -297,6 +467,7 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Email Błąd Znajdź + Pierwszy Wysokość Pomoc Ikona @@ -304,16 +475,20 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Margines wewnętrzny Wstaw Instaluj + Nieprawidłowe Wyrównaj Język - układ + Ostatni + Układ Ładowanie Zablokowany Zaloguj Wyloguj Wyloguj Makro + Obowiązkowy Przenieś + Więcej Nazwa Nowy Dalej @@ -323,19 +498,24 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Otwórz lub Hasło - Scieżka + Ścieżka Zastępczy ID - Proszę zaczekać... + Proszę czekać... Poprzedni Właściwości - E-mail aby otrzymywać dane z formularzy + E-mail, aby otrzymywać dane z formularzy Kosz Pozostało + Usuń Zmień nazwę Odnów + Wymagany + Odzyskaj Ponów próbę Uprawnienia Szukaj + Przepraszamy, ale nie możemy znaleźć tego, czego szukasz + Elementy nie zostały dodane Serwer Pokaż Pokaż stronę "wyślij" @@ -343,29 +523,79 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Sortuj Zatwierdź Typ - Szukaj + Wpisz, aby wyszukać... W górę - Update + Aktualizacja Aktualizacja Wyślij plik - Url + URL Użytkownik - Login + Nazwa użytkownika Wartość Widok Witaj... Szerokość Tak + Folder + Wyniki wyszukiwania Zmień kolejność Kolejność została zmieniona + Podgląd + Zmień hasło + do + Widok listy + Zapisywanie... + bieżący + Osadzony + wybrany + + + Czarny + Zielony + Żółty + Pomarańczowy + Niebieski + Czerwony + + + + Dodaj zakładkę + Dodaj właściwość + Dodaj edytora + Dodaj szablon + Dodaj węzeł dziecka + Dodaj dziecko + + Edytuj typ danych + + Nawiguj sekcje + + Skróty + Pokaż skróty + + Przełącz widok listy + Przełącznik możliwy jako korzeń + + Komentuj/Odkomentuj linie + Usuń linię + Kopiuj linie do góry + Kopiuj linie w dół + Przenieś linie w górę + Przenieś linie w dół + + Ogólne + Edytor + + Kolor tła Pogrubienie Kolor tekstu - Czcionka + Font Tekst + Strona @@ -374,73 +604,142 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Nie udało się zapisać pliku web.config. Zmodyfikuj parametry połączenia ręcznie. Twoja baza danych została znaleziona i zidentyfikowana jako Konfiguracja bazy danych - instaluj aby zainstalować bazę danych Umbraco %0%]]> - Dalej aby kontynuować.]]> - Nie odnaleziono bazy danych! Sprawdź czy informacje w sekcji "connection string" w pliku "web.config" są prawidłowe.

Aby kontynuować, dokonaj edycji pliku "web.config" (używając Visual Studio lub dowolnego edytora tekstu), przemieść kursor na koniec pliku, dodaj parametry połączenia do Twojej bazy danych w kluczu o nazwie "umbracoDbDSN" i zapisz plik.

Kliknij ponów próbę kiedy skończysz.
Tu znajdziesz więcej informacji na temat edycji pliku "web.config".

]]>
- Skontaktuj się z Twoim dostawą usług internetowych jeśli zajdzie taka potrzeba. W przypadku instalacji na lokalnej maszynie lub serwerze możesz potrzebować pomocy administratora.]]> - Naciśnij przycisk aktualizuj by zaktualizować swoją bazę danych do Umbraco %0%

Bez obaw - żadne dane nie zostaną usunięte i wszystko będzie działać jak należy!

]]>
- Naciśnij przycisk Dalej aby kontynuować.]]> - Dalej aby kontynuować kreatora instalacji.]]> + instaluj, aby zainstalować bazę danych Umbraco %0% +]]> + Dalej, aby kontynuować.]]> + Nie odnaleziono bazy danych! Sprawdź, czy informacje w sekcji "connection string" w pliku "web.config" są prawidłowe.

+

Aby kontynuować, dokonaj edycji pliku "web.config" (używając Visual Studio lub dowolnego edytora tekstu), przemieść kursor na koniec pliku, dodaj parametry połączenia do Twojej bazy danych w kluczu o nazwie "umbracoDbDSN" i zapisz plik.

+

+ Kliknij ponów próbę kiedy + skończysz.
+ Tu znajdziesz więcej informacji na temat edycji pliku "web.config".

]]>
+ + Skontaktuj się z Twoim dostawą usług internetowych jeśli zajdzie taka potrzeba. + W przypadku instalacji na lokalnej maszynie lub serwerze możesz potrzebować pomocy administratora.]]> + + Naciśnij przycisk aktualizuj, aby zaktualizować swoją bazę danych do Umbraco %0%

+

+ Bez obaw - żadne dane nie zostaną usunięte i wszystko będzie działać jak należy! +

+ ]]>
+ Naciśnij przycisk Dalej, aby + kontynuować.]]> + Dalej, aby kontynuować kreatora instalacji.]]> Hasło domyślnego użytkownika musi zostać zmienione!]]> - Konto domyślnego użytkownika została wyłączone lub nie ma on dostępu do Umbraco!

Żadne dotatkowe czynności nie są konieczne. Naciśnij Dalej aby kontynuować.]]> - Hasło domyślnego użytkownika zostało zmienione od czasu instalacji!

Żadne dotatkowe czynności nie są konieczne. Naciśnij Dalej aby kontynuować.]]> + Konto domyślnego użytkownika zostało wyłączone lub nie ma on dostępu do Umbraco!

Żadne dodatkowe czynności nie są konieczne. Naciśnij Dalej, aby kontynuować.]]> + Hasło domyślnego użytkownika zostało zmienione od czasu instalacji!

Żadne dodatkowe czynności nie są konieczne. Naciśnij Dalej, aby kontynuować.]]> Hasło zostało zmienione! - Umbraco tworzy domyślne konto użytkownika o loginie ('admin') i haśle ('default'). Jest ważne, by zmienić hasło na inne.

Ten krok sprawdzi, czy hasło domyślnego użytkownika powinno być zmienione.

]]>
Aby szybko wejść w świat Umbraco, obejrzyj nasze filmy wprowadzające Klikając przycisk dalej (lub modyfikując klucz UmbracoConfigurationStatus w pliku web.config), akceptujesz licencję na niniejsze oprogramowanie zgodnie ze specyfikacją w poniższym polu. Zauważ, że ta dystrybucja Umbraco składa się z dwoch licencji - licencja MIT typu open source dla kodu oraz licencja "Umbraco freeware", która dotyczy interfejsu użytkownika. Nie zainstalowane. Zmienione pliki i foldery Więcej informacji na temat ustalania pozwoleń dla Umbraco znajdziesz tutaj Musisz zezwolić procesowi ASP.NET na zmianę poniższych plików/folderów - Twoje ustawienia uprawnień są prawie idealne!

Umbraco będzie działało bez problemów, ale nie będzie możliwa instalacja pakietów, które są rekomendowane aby w pełni wykorzystać możliwości Umbraco.]]>
+ Twoje ustawienia uprawnień są prawie idealne!

+ Umbraco będzie działało bez problemów, ale nie będzie możliwa instalacja pakietów, które są rekomendowane, aby w pełni wykorzystać możliwości Umbraco.]]>
Jak to Rozwiązać Kliknij tutaj, aby przeczytać wersję tekstową - video tutorial pokazujący jak ustawić uprawnienia dostępu do folderów dla Umbraco albo przeczytaj wersję tekstową.]]> - Twoje ustawienia uprawnień mogą stanowić problem! Umbraco będzie działało bez problemów, ale nie będzie możliwa instalacja pakietów, które są rekomendowane aby w pełni wykorzystać możliwości Umbraco.]]> - Twoje ustawienia uprawnień nie są gotowe na Umbraco!

Aby Umbraco mogło działać musisz uaktualnić swoje ustawienia zabezpieczeń.]]>
- Twoje ustawienia uprawnień są idealne!

Umbraco będzie działać bez problemów i będzie można instalować pakiety!]]>
+ samouczek, pokazujący jak ustawić uprawnienia dostępu do folderów dla Umbraco, albo przeczytaj wersję tekstową.]]> + Twoje ustawienia uprawnień mogą stanowić problem! +

+ Umbraco będzie działało bez problemów, ale nie będzie możliwa instalacja pakietów, które są rekomendowane, aby w pełni wykorzystać możliwości Umbraco.]]>
+ Twoje ustawienia uprawnień nie są gotowe na Umbraco! +

+ Aby Umbraco mogło działać musisz uaktualnić swoje ustawienia zabezpieczeń.]]>
+ Twoje ustawienia uprawnień są idealne!

+ Umbraco będzie działać bez problemów i będzie można instalować pakiety!]]>
Rozwiązywanie problemów z folderami Kliknij ten link, aby uzyskać więcej informacji na temat problemów z ASP.NET i tworzeniem folderów. Ustawianie uprawnień dostępu do folderów - Umbraco potrzebuje uprawnień do zapisu/odczytu pewnych katalogów w celu przechowywania plików jak obrazki i PDF'y. Umbraco przechowuje także tymczasowe dane (aka: cache) aby zwiększyć wydajność Twojej strony. - Chce zacząć od zera - dowiedz się jak) Ciągle możesz wybrać, aby zainstalować Runway w późniejszym terminie. W tym celu przejdź do sekcji Deweloper i wybierz Pakiety.]]> + + Chcę zacząć od zera + dowiedz się jak) + Ciągle możesz wybrać, aby zainstalować Runway w późniejszym terminie. W tym celu przejdź do sekcji Deweloper i wybierz Pakiety. + ]]> Właśnie stworzyłeś czystą instalację platformy Umbraco. Co chcesz zrobić teraz? - Pakiet Runway zainstalowany pomyślnie - To jest nasza lista rekomendowanych modułów. Zaznacz te, które chcesz zainstalować lub wyświetl pełną listę modułów ]]> + Pakiet Runway został zainstalowany pomyślnie + + To jest nasza lista rekomendowanych modułów. Zaznacz te, które chcesz zainstalować lub wyświetl pełną listę modułów + ]]> Rekomendowane tylko dla doświadczonych użytkowników - Chce rozpocząć z prostą stroną - Pakiet "Runway" to prosta strona dostarczająca kilka podstawowych typów dokumentów i szablonów. Instalator może automatycznie zainstalować pakiet Runway za Ciebie, ale możesz w łatwy sposób edytować, rozszerzyć lub usunąć go. Nie jest on potrzebny i możesz doskonale używać Umbraco bez niego. Jednakże pakiet Runway oferuje łatwą podstawę bazującą na najlepszych praktych, która pozwolić Ci rozpocząć pracę w mgnieniu oka. Jeśli zdecydujesz się zainstalować pakiet Runway, możesz opcjonalnie wybrać podstawowe klocki zwane Modułami Runway, aby poprawić swoje strony.

Dołączone z pakietem Runway: Strona domowa, strona Jak rozpocząć pracę, strona Instalowanie Modułów.
Opcjonalne moduły:Górna nawigacja, Mapa strony, Formularz kontaktowy, Galeria.
]]>
+ Chcę rozpocząć z prostą stroną + + Pakiet "Runway" to prosta strona, dostarczająca kilku podstawowych typów dokumentów i szablonów. Instalator może automatycznie zainstalować pakiet Runway za Ciebie, + ale możesz w łatwy sposób edytować, rozszerzyć lub usunąć go. Nie jest on potrzebny i możesz doskonale używać Umbraco bez niego. + Jednakże pakiet Runway oferuje łatwą podstawę, bazującą na najlepszych praktykach, która pozwolić Ci rozpocząć pracę w mgnieniu oka. + Jeśli zdecydujesz się zainstalować pakiet Runway, możesz opcjonalnie wybrać podstawowe klocki zwane Modułami Runway, aby poprawić swoje strony. +

+ + Dołączone z pakietem Runway: Strona domowa, strona Jak rozpocząć pracę, strona Instalowanie Modułów.
+ Opcjonalne moduły:Górna nawigacja, Mapa strony, Formularz kontaktowy, Galeria. +
+ ]]>
Co to jest pakiet Runway Krok 1/5 Akceptacja licencji Krok 2/5: Konfiguracja bazy danych Krok 3/5: Sprawdzanie uprawnień plików Krok 4/5: Sprawdzanie zabezpieczeń Umbraco - Krok 5/5: Umbraco jest gotowy do pracy + Krok 5/5: Umbraco jest gotowe do pracy Dziękujemy za wybór Umbraco - Przeglądaj swoją nową stronę Pakiet Runway został zainstalowany, zobacz zatem jak wygląda Twoja nowa strona.]]> - Dalsza pomoc i informacje Zaczerpnij pomocy z naszej nagrodzonej społeczności, przeglądaj dokumentację lub obejrzyj niektóre darmowe filmy o tym jak budować proste strony, jak używać pakietów i szybki przewodnik po terminologii Umbraco]]> + Przeglądaj swoją nową stronę + Pakiet Runway został zainstalowany, zobacz zatem jak wygląda Twoja nowa strona.]]> + Dalsza pomoc i informacje + Zaczerpnij pomocy z naszej nagrodzonej społeczności, przeglądaj dokumentację lub obejrzyj niektóre darmowe filmy o tym, jak budować proste strony, jak używać pakietów i szybki przewodnik po terminologii Umbraco]]> Umbraco %0% zostało zainstalowane i jest gotowe do użycia - plik web.config i zaktualizować klucz AppSetting o nazwie UmbracoConfigurationStatus na dole do wartości '%0%'.]]> - rozpocząć natychmiast klikając przycisk "Uruchom Umbraco" poniżej.
Jeżeli jesteś nowy dla Umbraco znajdziesz mnóstwo materiałów na naszych stronach "jak rozpocząć".]]>
- Uruchom Umbraco Aby zarządzać swoją stroną po prostu otwórz zaplecze Umbraco i zacznij dodawać treść, aktualizować szablony i style lub dodawaj nową funkcjonalność]]> + plik web.config i zaktualizować klucz AppSetting o nazwie UmbracoConfigurationStatus na dole do wartości '%0%'.]]> + rozpocząć natychmiast klikając przycisk "Uruchom Umbraco" poniżej.
Jeżeli jesteś nowy dla Umbraco + znajdziesz mnóstwo materiałów na naszych stronach "jak rozpocząć".]]>
+ Uruchom Umbraco + Aby zarządzać swoją stroną po prostu otwórz zaplecze Umbraco i zacznij dodawać treść, aktualizować szablony i style lub dodawaj nową funkcjonalność]]> Połączenie z bazą danych nie zostało ustanowione. Umbraco wersja 3 Umbraco wersja 4 Zobacz - Umbraco %0% dla świżej instalacji lub aktualizacji z wersji 3.0.

Wciśnij "dalej", aby rozpocząć proces konfigruacji.]]>
+ Umbraco %0% dla świeżej instalacji lub aktualizacji z wersji 3.0. +

+ Wciśnij "dalej", aby rozpocząć proces konfigruacji.]]>
Kod języka Nazwa języka - TRANSLATE ME: 'You've been idle and logout will automatically occur in' - TRANSLATE ME: 'Renew now to save your work' + Z powodu bezczynności na stronie, nastąpi automatyczne wylogowanie + Wznów sesję teraz, aby zapisać swoją pracę + Szczęśliwej super niedzieli + Szczęśliwego maniakalnego poniedziałku + Szczęśliwego świetnego wtorku + Szczęśliwej niesamowitej środy + Szczęśliwego wyjątkowego czwartku + Szczęśliwego odjechanego piątku + Szczęśliwej cudownej soboty + Zaloguj się poniżej + Zaloguj się z + Sesja wygasła © 2001 - %0%
umbraco.com

]]>
- Witamy w Umbraco. Wprowadź swój login oraz hasło w polach poniżej: + Zapomniałeś hasła? + E-mail z linkiem do zresetowania hasła zostanie wysłany na podany adres + E-mail z instrukcjami do zresetowania hasła zostanie wysłany, jeśli zgadza się z naszą bazą danych + Powrót do formularza logowania + Proszę wpisać nowe hasło + Twoje hasło zostało zmienione + Link, na który kliknąłeś jest niewłaściwy lub wygasł + Umbraco: Resetowanie hasła + + Twoja nazwa użytkownika do zalogowania się w Umbraco back-office to: %0%

Kliknij tutaj, aby zresetować Twoje hasło lub kopiuj/wklej ten URL w przeglądarce:

%1%

]]> +
Panel zarządzania @@ -451,34 +750,107 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Wybierz stronę powyżej... %0% zostało skopiowane do %1% Wybierz, gdzie dokument %0% ma zostać skopiowany - %0% został przesunięty do %1% + %0% został przeniesiony do %1% Wskaż gdzie dokument %0% ma zostać przeniesiony - został wybrany jako korzeń nowej treści, kliknik 'ok' poniżej. + został wybrany jako korzeń nowej zawartości, kliknik 'ok' poniżej. Nie wskazano węzła, proszę wybrać węzeł z listy powyżej przed kliknięciem "ok" Typ bieżącego węzła nie jest dozwolony dla wybranego węzła Bieżący węzeł nie może być przeniesiony do jednej z jego podstron - TRANSLATE ME: 'The action isn't allowed since you have insufficient permissions on 1 or more child documents.' + Bieżący węzeł nie może istnieć w korzeniu + Działanie jest niedozwolone, ponieważ nie masz odpowiednich uprawnień w 1 lub więcej dokumentach dzieci. + Powiąż skopiowane elementy z oryginalnymi Edytuj powiadomienie dla %0% - + Witaj %0%

-Miłego dnia!]]>
- Witaj %0%

To jest automatyczny e-mail, wysłany aby poinformować Cię, że polecenie '%1%' zostało wykonane na stronie '%2%' przez użytkownika '%3%'

Podsumowanie zmian:

%6%

Miłego dnia!

Pozdrowienia od robota Umbraco

]]>
+

To jest automatyczny e-mail, wysłany, aby poinformować Cię, że polecenie '%1%' + zostało wykonane na stronie '%2%' + przez użytkownika '%3%' +

+ +

+

Podsumowanie zmian:

+ + %6% +
+

+ + + +

Miłego dnia!

+ Pozdrowienia od robota Umbraco +

]]> [%0%] Powiadomienie o %1% wykonane na %2% Powiadomienie - Wskaż pakiet z twojego komputera, poprzez kliknięcie na przycisk 'Przeglądaj' i wskaż gdzie jest zapisany. Pakiety Umbraco przeważnie posiadają rozszerzenie ".umb" lub ".zip". + + i wskaż gdzie jest zapisany. Pakiety Umbraco przeważnie posiadają rozszerzenie ".umb" lub ".zip". + ]]> + Upuść, aby załadować + lub kliknij tutaj, aby wybrać pliki + Załaduj pakiet + Zainstaluj lokalny pakiet poprzez wybranie go ze swojego komputera. Instaluj jedynie te pakiety, z zaufanych i znanych Tobie źródeł + Załaduj kolejny pakiet + Anuluj i załaduj kolejny pakiet + Licencja + Zgadzam się + zasady użytkowania + Zainstaluj pakiet + Zakończ + Zainstalowane pakiety + Nie masz żadnych zainstalowanych pakietów + 'Pakiety' w prawym górnym rogu ekranu]]> + Szukaj pakietów + Wyniki dla + Nie mogliśmy znaleźć niczego dla + Spróbuj wyszukać kolejny pakiet lub przeszukaj kategorie pakietów + Popularne + Nowe wydania + ma + punktów karmy + Informacja + Właściciel + Kontrybutor + Utworzone + Obecna wersja + wersja .NET + Pobrania + Polubienia + Zgodność + Według raportów członków społeczności, ten pakiet jest zgodny z następującymi wersjami Umbraco. Pełna zgodność nie może być zagwarantowana dla wersji zaraportowanych poniżej 100% + Zewnętrzne źródła Autor Demonstracja Dokumentacja Metadane pakietu Nazwa pakietu Pakiet nie zawiera żadnych elementów -
Możesz bezpiecznie go usunąć z systemu poprzez kliknięcie na przycisku "odinstaluj pakiet"]]>
+
+ Możesz bezpiecznie go usunąć z systemu poprzez kliknięcie na przycisku "odinstaluj pakiet"]]>
Nie ma dostępnych aktualizacji Opcje pakietu Opis pakietu @@ -487,13 +859,26 @@ Miłego dnia!]]> Pakiet został odinstalowany Pakiet został pomyślnie odinstalowany Odinstaluj pakiet - span style="color: Red; font-weight: bold;">Uwaga: wszystkie elementy, media itp w zależności od elementów, które usuwasz przestaną działać, i mogą spowodować niestabilność systemu, więc odinstalowuj z uwagą. W przypadku problemów kontaktuj się z autorem pakietu.]]> + + Uwaga: wszystkie elementy, media, itp. w zależności od elementów, które usuwasz, przestaną działać i mogą spowodować niestabilność systemu, + więc odinstalowuj z uwagą. W przypadku problemów skontaktuj się z autorem pakietu.]]> Pobierz aktualizację z repozytorium Aktualizuj pakiet Instrukcja aktualizacji Jest dostępna aktualizacja dla tego pakietu. Możesz ją pobrać wprost z repozytorium pakietów Umbraco. Wersja pakietu + Historia wersji pakietu Odwiedź stronę pakietu + Pakiet jest już zainstalowany + Ten pakiet nie może być zainstalowany, ponieważ wymaga Umbraco w wersji przynajmniej %0% + Odinstalowywanie... + Pobieranie... + Importowanie... + Instalowanie... + Restartowanie, proszę czekać... + Wszystko skończone, Twoja przeglądarka się teraz odświeży, proszę czekać... + Proszę kliknąć Zakończ, aby zakończyć instalację i przeładować stronę. + Wgrywanie pakietu... Wklej z zachowaniem formatowania (Nie zalecane) @@ -506,46 +891,71 @@ Miłego dnia!]]> użyj grup członkowskich Umbraco ]]> Musisz utworzyć grupę przed użyciem uwierzytelniania opartego na rolach Strona błędu - Używana kiedy użytkownicy są zalogowani, ale nie posiadają dostępu + Używana, kiedy użytkownicy są zalogowani, ale nie posiadają dostępu Wybierz sposób ograniczenia dostępu do tej strony %0% jest teraz zabezpieczona Ze strony %0% usunięto zabezpieczenia dostępu Strona logowania Wybierz stronę z formularzem logowania Usuń ochronę - Wybierz strony, które zawierają formularz logowania i komunikatów o błędach + Wybierz strony, które zawierają formularz logowania i komunikaty o błędach Wybierz role, które mają mieć dostęp do tej strony - Ustaw login i hasło dla tej strony + Ustaw nazwę użytkownika i hasło dla tej strony Ochrona pojedynczego użytkownika - Jeżeli chcesz ustawić prostą ochronę używając pojedynczego loginu i hasła + Jeżeli chcesz ustawić prostą ochronę używając pojedynczej nazwy użytkownika i hasła - %0% nie może zostać opublikowany, ze względu na odwołanie akcji przez rozszerzenie firm trzecich + + + + + + + Dołącz nieopublikowane węzły pochodne (dzieci) Publikacja w toku - proszę czekać... Opublikowano %0% z %1% stron... %0% został opublikowany %0% oraz podstrony zostały opublikowane - Publikuj %0% z wszytkimi podstronami - OK , aby publikować % 0% i spowodować upublicznienie całej treści.

Możesz opublikować tą stronę wraz ze wszystkimi podstronami zaznaczając poniżej publikuj wszystkie węzły pochodne]]>
+ Publikuj %0% ze wszytkimi podstronami + OK , aby publikować % 0% i spowodować upublicznienie całej zawartości.

+ Możesz opublikować tą stronę wraz ze wszystkimi podstronami zaznaczając poniżej publikuj wszystkie węzły pochodne + ]]>
+ + + Nie skonfigurowałeś żadnych zaakceptowanych kolorów - Dodaj link zewnętrzny - Dodaj link wewnętrzny - Dodaj - Opis - Strona wewnętrzna - URL - Przesuń w dół - Przesuń do góry + Wpisz link zewnętrzny + Wybierz link wewnętrzny + Podpis + Link Otwórz w nowym oknie - Usuń link + Wpisz nowy podpis + Wpisz link + + + Resetuj + Zdefiniuj przycięcie + Ustaw alias dla przycięcia, a także jego domyślną szerokość i długość + Zapisz przycięcie + Dodaj nowe przycięcie Aktualna wersja - Red tekst nie będzie pokazany w wybranej wersji, zielony tekst został dodany]]> + Czerwony tekst nie będzie pokazany w wybranej wersji, zielony tekst został dodany]]> Dokument został przywrócony - Tu widać wybraną wersję jako html, jeżeli chcesz zobaczyć różnicę pomiędzy 2 wersjami w tym samym czasie, użyj podglądu róznic + Tu widać wybraną wersję jako html, jeżeli chcesz zobaczyć różnicę pomiędzy 2 wersjami w tym samym czasie, użyj podglądu różnic Cofnij do Wybierz wersję Zobacz @@ -566,6 +976,15 @@ Miłego dnia!]]> Statystyki Tłumaczenie Użytkownicy + Pomoc + Formularze + Analizy + + + idź do + Tematy pomocy dla + Rozdziały filmów dla + Najlepsze filmy-samouczki Umbraco Domyślny szablon @@ -575,19 +994,33 @@ Miłego dnia!]]> Typ węzła Typ Arkusz styli + Skrypt Właściwości arkusza styli Zakładka Nazwa zakładki Zakładki + Włączono Główny Typ Treści + Ten Typ Treści używa + jako Główny Typ Treści. Zakładki Głównego Typu Treści nie są wyświetlone i mogą być edytowane jedynie w samym Głównym Typie Treści + Żadne właściwości nie zostały zdefiniowane dla tej zakładki. Kliknij w link "dodaj nową właściwość", który znajduje się na górze strony, aby stworzyć nową właściwość. + Główny Typ Dokumentu + Stwórz pasujący szablon + Dodaj ikonę - Sort order - Creation date + Porządek sortowania + Data utworzenia Sortowanie zakończone. - Przesuń poszczególne elementy w górę oraz w dół aż będą w odpowiedniej kolejności, lub kliknij na nagłówku kolumny aby posortować całą kolekcję elementów -
Nie zamykaj tego okna podczas sortowania]]>
+ Przesuń poszczególne elementy w górę oraz w dół aż będą w odpowiedniej kolejności lub kliknij na nagłówku kolumny, aby posortować całą kolekcję elementów + + Walidacja + Błędy walidacji muszą zostać naprawione zanim element będzie mógł być zapisany + Nie powiodło się + Niewystarczające uprawnienia użytkownika, nie można zakończyć operacji + Anulowane + Operacja została anulowana przez dodatek firmy trzeciej Publikacja została przerwana poprzez dodatek firmy trzeciej Właściwość typu już istnieje Właściwość typu została utworzona @@ -596,34 +1029,39 @@ Miłego dnia!]]> Zakładka została zapisana Zakładkę utworzono Zakładkę usunięto - Usunięto zakładkę o id:%0% + Usunięto zakładkę o ID:%0% Arkusz stylów nie został zapisany Arkusz stylów został zapisany Arkusz stylów został zapisany bez żadnych błędów Typ danych został zapisany Element słownika został zapisany - Publikacja nie powiodła się ponieważ rodzic węzła nie jest opublikowany + Publikacja nie powiodła się, ponieważ rodzic węzła nie jest opublikowany Treść została opublikowana i jest widoczna na stronie Treść została zapisana Pamiętaj, aby opublikować, aby zmiany były widoczne Wysłano do zatwierdzenia Zmiany zostały wysłane do akceptacji + Media zostały zapisane + Media zostały poprawnie zapisane Członek został zapisany Właściwość arkusza stylów została zapisana Arkusz stylów został zapisany Szablon został zapisany Błąd przy zapisie danych użytkownika (sprawdź log) Użytkownik został zapisany + Typ użytkownika został zapisany Plik nie został zapisany Plik nie został zapisany. Sprawdź uprawnienia dostępu do pliku Plik został zapisany Plik został zapisany bez żadnych błędów Język został zapisany - Skrypt python nie został zapisany - Wystąpiły błędy, skrypt nie może zostać zapisany - Skrypt python został zapisany - Brak błędów w skrypcie python + Typ mediów został zapisany + Typ członka został zapisany + Skrypt Python nie został zapisany + Wystąpiły błędy, skrypt Python nie może zostać zapisany + Skrypt Python został zapisany + Brak błędów w skrypcie Python Szablon nie został zapisany Proszę się upewnić że nie ma dwóch szablonów o tym samym aliasie Szablon został zapisany @@ -633,6 +1071,16 @@ Miłego dnia!]]> Nie można zapisać XSLT, sprawdź uprawnienia dostępu do pliku Zapisano XSLT XSLT nie zawiera błedów + Cofnięto publikację treści + Częściowy Widok został zapisany + Częściowy Widok został zapisany bez błędów! + Częściowy Widok nie został zapisany + Wystąpił błąd podczas zapisywania pliku. + Widok skryptu został zapisany + Widok skryptu został zapisany bez błędów! + Widok skryptu nie został zapisany + Wystąpił błąd podczas zapisywania pliku. + Wystąpił błąd podczas zapisywania pliku. Używaj składni CSS np.: h1, .czerwonyNaglowek, .niebieskiTekst @@ -642,86 +1090,264 @@ Miłego dnia!]]> Podgląd Style + Edytuj szablon + + Sekcje Wstaw obszar zawartości Wstaw miejsce dla obszaru zawartości + + Wstaw + Wybierz, co chcesz wstawić do swojego szablonu + Wstaw element słownika - Wstaw makro - Wstaw pole strony Umbraco + Element słownika to miejsce, gdzie można wstawić przetłumaczony tekst, co ułatwia tworzenie projektów dla wielojęzycznych stron. + + Makro + + Makro to konfigurowalny komponent, który sprawdzi się + przy wielokrotnie używanych częściach Twojego projektu, kiedy potrzebujesz opcji dostarczenia parametrów, + takich jak galerie, formularze, czy listy. + + + Wartość + Wyświetla wartość danego pola z bieżącej strony z opcjami modyfikacji wartości lub powrotu do alernatywnych wartości. + + Częściowy Widok + + Częściowy Widok to oddzielny szablon pliku, który może być renderowany wewnątrz innego + szablonu, sprawdzi się w ponownym używaniu markupu lub oddzielaniu złożonych szablonów do oddzielnych plików. + + Główny szablon - Szybki przewodnik po tagach szablonu Umbraco + Brak głównego szablonu + Brak głównego + + Renderuj szablon dziecka + + @RenderBody(). + ]]> + + + + Zdefiniuj nazwaną sekcję + + @section { ... }. Może być to renderowane w + określonym obszarze rodzica tego szablonu, poprzez użycie @RenderSection. + ]]> + + + Renderuj nazwaną sekcję + + @RenderSection(name). + To renderuje obszar w szablonie dziecka, który jest opakowany w odpowiednią definicję @section [name]{ ... }. + ]]> + + + Nazwa Sekcji + Sekcja jest wymagana + + Jeśli wymagana, szablon dziecka musi zawierać definicję @section, w przeciwnym przypadku wystąpi błąd. + + + + Konstruktor zapytań + Zbuduj zapytanie + Element zwrócony, w + + Chcę + całą zawartość + zawartość typu "%0%" + z + mojej strony + gdzie + i + + jest + nie jest + przed + przed (włączając wybraną datę) + po + po (włączając wybraną datę) + równa się + nie równa się + zawiera + nie zawiera + większe niż + większe lub równe niż + mniejsze niż + mniejsze lub równe niż + + ID + Nazwa + Data Utworzenia + Data Ostatniej Aktualizacji + + sortuj + rosnąco + malejąco + Szablon + + - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied + Wybierz typ treści + Wybierz układ + Dodaj wiersz + Dodaj zawartość + Upuść zawartość + Zastosowano ustawienia + + Ta zawartość nie jest tu dozwolona + Ta zawartość jest tu dozwolona + + Kliknij, żeby osadzić + Kliknij, żeby dodać obraz + Podpis obrazu... + Pisz tutaj... + + Układy Siatki + Układy to ogólne pole pracy dla edytora siatki, przeważnie będziesz potrzebować tylko jednego lub dwóch różnych układów + Dodaj Układ Siatki + Dostosuj układ przez ustawienie szerokości kolumn i dodanie dodatkowych sekcji + Konfiguracja rzędów + Rzędy to predefiniowane komórki ułożone poziomo + Dodaj konfigurację rzędu + Dostosuj rząd poprzez ustawienie szerokości komórki i dodanie dodatkowych komórek + + Kolumny + Całkowita liczba wszystkich kolumn w układzie siatki + + Ustawienia + Konfiguruj jakie ustawienia może zmieniać edytor + + Style + Konfiguruj jakie style może zmieniać edytor + + Ustawienia zostaną zapisane tylko jeśli wprowadzona konfiguracja json jest prawidłowa + + Zezwól wszystkim edytorom + Zezwól na konfigurację wszystkich rzędów + Ustaw jako domyślne + Wybierz dodatkowe + Wybierz domyślne + zostały dodane + + + + + Kompozycje + Nie dodałeś żadnych zakładek + Dodaj nową zakładkę + Dodaj kolejną zakładkę + Odziedziczone z + Dodaj właściwość + Wymagana etykieta + + Włącz widok listy + Konfiguruje element treści, aby pokazać sortowaną i możliwą do przeszukiwania listę jego dzieci, dzieci nie będą wyświetlone w drzewie + + Dozwolone Szablony + Wybierz, które szablony edytorzy będą mogli używać dla zawartości tego typu + + Zezwól jako korzeń + Zezwól edytorom na tworzenie zawartości tego typu w korzeniu drzewa treści + Tak - zezwól na zawartość tego typu w korzeniu + + Dozwolone typy węzłów dzieci + Zezwól na tworzenie zawartości określonych typów pod zawartością tego typu - This content is not allowed here - This content is allowed here + Wybierz węzeł dziecka - Click to embed - Click to insert image - Image caption... - Write here... + Odziedzicz zakładki i właściwości z istniejącego typu dokumentu. Nowe zakładki będą dodane do bieżącego typu dokumentu lub złączone jeśli zakładka z identyczną nazwą już istnieje. + Ten typ zawartości jest używany w kompozycji, przez co sam nie może być złożony. + Brak możliwych typów zawartości do użycia jako kompozycja. - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells + Dostępni edytorzy + Użyj ponownie + Ustawienia edytora - Columns - Total combined number of columns in the grid layout + Konfiguracja - Settings - Configure what settings editors can change + Tak, usuń - Styles - Configure what styling editors can change + zostało przeniesione poniżej + zostało skopiowane poniżej + Wybierz folder do przeniesienia + Wybierz folder do skopiowania + do w strukturze drzewa poniżej - Settings will only save if the entered json configuration is valid + Wszystkie typy Dokumentów + Wszystkie Dokumenty + Wszystkie elementy mediów + + używający tego typu dokumentu zostanie usunięty na stałe, proszę potwierdź czy chcesz usunąć także te. + używający tych mediów zostanie usunięty na stałe, proszę potwierdź czy chcesz usunąć także te. + używający tego typu członka zostanie usunięty na stałe, proszę potwierdź czy chcesz usunąć także te + + i wszystkie dokumenty, używające tego typu + i wszystkie media, używające tego typu + i wszyscy członkowie, używający tego typu + + używający tego edytora będzie zaktualizowany o nowe ustawienia + + Członek może edytować + Pokaż na profilu członka - Allow all editors - Allow all row configurations + + Dodaj pole zastępcze + Pole zastępcze + Dodaj domyślną wartość + Domyślna wartość Pole alternatywne Tekst alternatywny Wielkość liter + Kodowanie Wybierz pole Konwertuj złamania wiersza + Tak, konwertuj złamania wiersza Zamienia złamania wiersza na html-tag &lt;br&gt; + Niestandardowe Pola Tak, tylko data + Format i kodowanie Formatuj jako datę + Formatuj wartość jako datę lub jako datę i czas, zgodnie z aktywną kulturą Kodowanie HTML Zamienia znaki specjalne na ich odpowiedniki HTML Zostanie wstawione za wartością pola Zostanie wstawione przed wartością pola małe znaki + Modyfikuj dane wyjściowe Nic + Próbka danych wyjściowych Wstaw za polem Wstaw przed polem Rekurencyjne - Usuń znaki paragrafu - Usuwa wszystkie &lt;P&gt; z początku i końca tekstu - WIELKIE LITERY + Tak, spraw, aby było to rekurencyjne + Separator + Standardowe Pola + Wielkie litery Kodowanie URL Formatuje znaki specjalne w URLach - Zostanie użyte tylko wtedy gdy wartość pola jest pusta - To pole jest używane tylko wtedy gdy główne pole jest puste + Zostanie użyte tylko wtedy, gdy wartość pola jest pusta + To pole jest używane tylko wtedy, gdy główne pole jest puste Tak, z czasem. Separator: Zadania przypisane dla Ciebie - przypisane do Ciebie
. Aby zobaczyć szczegółowe informacje wraz z komentarzami, kliknij na "Szczegóły" lub po prostu na nazwę strony. Możesz również pobrać stronę jako XML poprzez kliknięcie na link "Pobierz XML".
Aby zamknąć zadanie tłumaczenia, proszę w podglądzie szczegółowym kliknąć przycisk "Zamknij".]]> + przypisane do Ciebie
. Aby zobaczyć szczegółowe informacje wraz z komentarzami, kliknij na "Szczegóły" lub po prostu na nazwę strony. + Możesz również pobrać stronę jako XML poprzez kliknięcie na link "Pobierz XML".
+ Aby zamknąć zadanie tłumaczenia, proszę kliknąć "Zamknij" w podglądzie szczegółowym. + ]]> zamknij zadanie Szczegóły tłumaczenia Pobierz wszystkie tłumaczenia jako XML @@ -729,19 +1355,37 @@ Miłego dnia!]]> Pobierz XML DTD Pola Włączając podstrony - to jest automatyczny mail informujący że dokument %1% został zgłoszony jako wymagający tłumaczenia na '%5%' przez %2%. Wejdź na http://%3%/translation/details.aspx?id=%4% aby edytować. Lub zaloguj się do Umbraco aby zobaczyć wszystkie zadania do tłumaczenia http://%3%.
Życzę miłego dnia! Umbraco robot]]>
+ [%0%] Tłumaczeń dla %1% Nie znaleziono tłumaczy. Proszę utwórz tłumacza przed wysłaniem zawartości do tłumaczenia Zadania stworzone przez Ciebie - stworzone przez Ciebie. Aby zobaczyć szczegółowe informacje wraz z komentarzami, kliknij na "Szczegóły" lub na nazwę strony. Możesz również pobrać stronę jako XML poprzez kliknięcie na link "Pobierz XML".
Aby zamknąć zadanie tłumaczenia, proszę w podglądzie szczegółowym kliknąć przycisk "Zamknij".]]>
+ stworzone przez Ciebie. Aby zobaczyć szczegółowe informacje wraz z komentarzami, + kliknij na "Szczegóły" lub na nazwę strony. Możesz również pobrać stronę jako XML poprzez kliknięcie na link "Pobierz XML". + Aby zamknąć zadanie tłumaczenia, proszę kliknąć "Zamknij" w podglądzie szczegółowym. + ]]> Strona '%0%' została wysłana do tłumaczenia + Proszę wybrać język, na jaki zawartość powinna zostać przetłumaczona Wyślij stronę '%0%' do tłumaczenia Przypisane przez Zadanie otwarte Liczba słów Przetłumacz na Tłumaczenie zakończone. - Możesz podglądnąć stronę, którą właśnie przetłumaczyłeś, poprzez kliknięcie poniżej. Jeżeli strona oryginalna istnieje, możesz porównać obie wersje + Możesz podejrzeć stronę, którą właśnie przetłumaczyłeś, poprzez kliknięcie poniżej. Jeżeli strona oryginalna istnieje, możesz porównać obie wersje Błąd tłumaczenia, plik XML może być uszkodzony Opcje tłumaczeń Tłumacz @@ -754,8 +1398,8 @@ Miłego dnia!]]> Typy danych Słownik Zainstalowane pakiety - TRANSLATE ME: 'Install skin' - TRANSLATE ME: 'Install starter kit' + Zainstaluj skórkę + Zainstaluj Starter Kit Języki Zainstaluj pakiet lokalny Makra @@ -765,21 +1409,25 @@ Miłego dnia!]]> Role Typ członka Typy dokumentów + Typy relacji Pakiety Pakiety Pliki Python Zainstaluj z repozytorium Zainstaluj Runway Moduły Runway - TRANSLATE ME: 'Scripting Files' + Pliki skryptowe Skrypty Arkusze stylów Szablony Pliki XSLT + Analizy + Częściowe Widoki + Pliki Makro Częściowych Widoków Aktualizacja jest gotowa - Gotowe jest %0%, kliknij tutaj aby pobrać + Gotowe jest %0%, kliknij tutaj, aby pobrać Brak połączenia z serwerem Wystąpił błąd podczas sprawdzania aktualizacji. Przeglądnij trace-stack dla dalszych informacji @@ -787,8 +1435,10 @@ Miłego dnia!]]> Administrator Pole kategorii Zmień hasło! - TRANSLATE ME: 'You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button' - Zawartość + Nowe hasło + Potwierdź nowe hasło + Możesz zmienić swoje hasło w Umbraco Back Office przez wypełnienie formularza poniżej i kliknięcie przycisku "Zmień hasło" + Kanał zawartości Opis Wyłącz użytkownika Typ dokumentu @@ -799,22 +1449,173 @@ Miłego dnia!]]> Węzeł początkowy w bibliotece mediów Sekcje Wyłącz dostęp do Umbraco + Stare hasło Hasło + Zresetuj hasło Twoje hasło zostało zmienione! Proszę potwierdź nowe hasło! Wprowadź nowe hasło Nowe hasło nie może byc puste! - TRANSLATE ME: 'There was a difference between the new password and the confirmed password. Please try again!' - TRANSLATE ME: 'The confirmed password doesn't match the new password!' + Bieżące hasło + Bieżące hasło jest nieprawidłowe + Nowe hasło i potwierdzenie nowego hasła nie są identyczne. Spróbuj ponownie! + Potwierdzone hasło nie jest identyczne z nowym hasłem! Zastąp prawa dostępu dla węzłów potomnych Aktualnie zmieniasz uprawnienia dostępu do stron: Wybierz strony, którym chcesz zmienić prawa dostępu Przeszukaj wszystkie węzły potomne - Węzeł początkowy w treści + Węzeł początkowy w zawartości Nazwa użytkownika Prawa dostępu użytkownika - Typ - Typy użytkowników Pisarz + Tłumacz + Zmień + Twój profil + Twoja historia + Sesja wygaśnie za + + + Walidacja + Waliduj jako e-mail + Waliduj jako numer + Waliduj jako URL + ...lub wpisz niestandardową walidację + Pole jest wymagane + Wprowadź wyrażenie regularne + Musisz dodać przynajmniej + Możesz mieć jedynie + elementy + wybrane elementy + Niepoprawna data + To nie jest numer + Niepoprawny e-mail + + + + Wartość jest ustawiona na rekomendowaną wartość: '%0%'. + Wartość została ustawiona na '%1%' dla XPath '%2%' w pliku konfiguracyjnym '%3%'. + Oczekiwana jest wartość '%1%' dla '%2%' w pliku konfiguracyjnym '%3%', ale znaleziono '%0%'. + Znaleziono nieoczekiwaną wartość '%0%' dla '%2%' w pliku konfiguracyjnym '%3%'. + + + Niestandardowe błędy są ustawione na '%0%'. + Niestandardowe błędy są obecnie ustawione na '%0%'. Zaleca się ustawienie ich na '%1%' przed wypuszczeniem strony na produkcję. + Niestandardowe błędy zostały z powodzeniem ustawione na '%0%'. + + MacroErrors są ustawione na '%0%'. + MacroErrors są ustawione na '%0%' co uniemożliwi częściowe lub całkowite załadowanie stron w Twojej witrynie jeśli wystąpią jakiekolwiek błędy w makro. Korekta ustawi wartość na '%1%'. + MacroErrors są teraz ustawione na '%0%'. + + + Try Skip IIS Custom Errors jest ustawione na '%0%' a Ty używasz IIS w wersji '%1%'. + Try Skip IIS Custom Errors wynosi obecnie '%0%'. Zalecane jest ustawienie go na '%1%' dla Twojego IIS w wersji (%2%). + Try Skip IIS Custom Errors ustawiono z powodzeniem na '%0%'. + + + Plik nie istnieje: '%0%'. + '%0%' w pliku konfiguracyjnym '%1%'.]]> + Wystąpił błąd, sprawdź logi, aby wyświetlić pełen opis błędu: %0%. + + Członkowie - Suma XML: %0%, Suma: %1%, Suma niepoprawnych: %2% + Media - Suma XML: %0%, Suma: %1%, Suma niepoprawnych: %2% + Zawartość - Suma XML: %0%, Suma opublikowanych: %1%, Suma niepoprawnych: %2% + + Certifikat Twojej strony jest poprawny. + Błąd walidacji certyfikatu: '%0%' + Certyfikat SSL Twojej strony wygasł. + Certyfikat SSL Twojej strony wygaśnie za %0% dni. + Błąd pingowania adresu URL %0% - '%1%' + Oglądasz %0% stronę używając HTTPS. + appSetting 'umbracoUseSSL' został ustawiony na 'false' w Twoim pliku web.config. Po uzyskaniu dostępu do strony, używając HTTPS, powinieneś go ustawić na 'true'. + appSetting 'umbracoUseSSL' został ustawiony na '%0%' w Twoim pliku web.config, Twoje ciasteczka są %1% ustawione jako bezpieczne. + Nie można zaktualizaować ustawień 'umbracoUseSSL' w Twoim pliku web.config file. Błąd: %0% + + + Włącz HTTPS + Ustawia umbracoSSL na 'true' w appSettings pliku web.config. + appSetting 'umbracoUseSSL' jest teraz ustawione na 'true' w Twoim pliku web.config, Twoje ciasteczka będą oznaczone jako bezpieczne. + + Napraw + Nie można naprawić sprawdzenia z wartością typu porównania 'ShouldNotEqual'. + Nie można naprawić sprawdzenia z wartością typu porównania 'ShouldEqual' z wprowadzoną wartością. + Nie wprowadzono wartości do naprawy sprawdzenia. + + Tryb kompilacji debugowania jest wyłączony. + Tryb kompilacji debugowania jest obecnie włączony. Zaleca się wyłączenie tego ustawienia przed wypuszczeniem strony na produkcję. + Tryb komplikacji debugowania został wyłączony z powodzeniem. + + Tryb śledzenia jest wyłączony. + Tryb śledzenia jest obecnie włączony. Zaleca się wyłączenie tego ustawienia przed wypuszczeniem strony na produkcję. + Tryb śledzenia został wyłączony z powodzeniem + + Wszystkie foldery mają ustawione poprawne ustawienia. + + %0%.]]> + %0%. Jeśli nie będzie nic w nich pisane, żadne działania nie muszą być podejmowane.]]> + + Wszystkie pliki mają ustawione poprawne uprawnienia. + + %0%.]]> + %0%. Jeśli nie będzie nic w nich pisane, żadne działania nie muszą być podejmowane.]]> + + X-Frame-Options używany do kontrolowania czy strona może być IFRAME'owana przez inną został znaleziony.]]> + X-Frame-Options używany do kontrolowania czy strona może być IFRAME'owana przez inną nie został znaleziony.]]> + Ustaw nagłówek w Config + Dodaje wartość do sekcji httpProtocol/customHeaders pliku web.config, aby zapobiec IFRAME'owania strony przez inne witryny. + Ustawienie do tworzenia nagłówka, zapobiegającego IFRAME'owania strony przez inne witryny zostało dodane do Twojego pliku web.config. + Nie można zaktualizować pliku web.config. Błąd: %0% + + + %0%.]]> + Nie znaleziono żadnych nagłówków, ujawniających informacji o technologii strony. + + Nie znaleziono system.net/mailsettings w pliku Web.config. + Host nie jest skonfigurowany w sekcji system.net/mailsettings pliku Web.config. + Ustawienia SMTP są skonfigurowane poprawnie i serwis działa według oczekiwań. + Nie można połączyć się z serwerem SMTP skonfigurowanym z hostem '%0%' i portem '%1%'. Proszę sprawdzić ponownie, czy ustawienia system.net/mailsettings w pliku Web.config są poprawne. + + %0%.]]> + %0%.]]> + + + Wyłącz śledzenie URL + Włącz śledzenie URL + Oryginalny URL + Przekierowane do + Nie stworzono żadnych przekierowań + Kiedy nazwa opublikowanej strony zostanie zmieniona lub zostanie ona przeniesiona, zostanie stworzone automatyczne przekierowanie na nową stronę. + Usuń + Czy jesteś pewien, że chcesz usunąć przekierowanie z '%0%' do '%1%'? + Przekierowanie URL zostało usunięte. + Wystąpił błąd podczas usuwania przekierowania URL. + Czy jesteś pewien, że chcesz wyłączyć śledzenie URL? + Śledzenie URL zostało wyłączone. + Wystąpił błąd podczas wyłączania śledzenia URL, więcej informacji znajdziesz w pliku z logami. + Śledzenie URL zostało włączone. + Wystąpił błąd podczas włączania śledzenia URL, więcej informacji znajdziesz w pliku z logami. + + + Brak elementów słownika do wyboru + + + pozostało znaków - \ No newline at end of file + diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/pt.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/pt.xml index 8fa5dc3d..a0becf13 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/pt.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/pt.xml @@ -15,8 +15,6 @@ Desabilitar Esvaziar Lixeira Exportar Tipo de Documento - Exportar para .NET - Exportar para .NET Importar Tipo de Documento Importar Pacote Editar na Tela @@ -32,7 +30,6 @@ Enviar para Publicação Enviar para Tradução Classificar - Enviar para publicação Traduzir Atualizar @@ -392,15 +389,6 @@ O usuário padrão foi desabilitado ou não tem acesso à Umbraco!

Nenhuma ação posterior precisa ser tomada. Clique Próximo para prosseguir.]]> A senha do usuário padrão foi alterada com sucesso desde a instalação!

Nenhuma ação posterior é necessária. Clique Próximo para prosseguir.]]> Senha foi alterada! - - Umbraco cria um usuário padrão com o login ('admin') e senha ('default'). É importante que a senha seja alterada para algo único. -

-

- Este passo irá checar a senha do usuário padrão e sugerir uma alteração se necessário. -

- - ]]>
Comece com o pé direito, assista nossos vídeos introdutórios Ao clicar no próximo botão (ou modificando o UmbracoConfigurationStatus no web.config), você aceita a licença deste software cmo especificado na caixa abaixo. Note que esta distribuição de Umbraco consiste em duas licenças diferentes, a licença aberta MIT para a framework e a licença de software livre (freeware) Umbraco que cobre o UI. Nenhum instalado ainda. @@ -658,7 +646,7 @@ Você pode publicar esta página e todas suas sub-páginas ao selecionar pub Creation date Classificação concluída. Arraste os diferentes itens para cima ou para baixo para definir como os mesmos serão arranjados. Ou clique no título da coluna para classificar a coleção completa de itens -
Não feche esta janela durante a classificação]]>
+ Publicação foi cancelada por add-in de terceiros @@ -783,8 +771,6 @@ Você pode publicar esta página e todas suas sub-páginas ao selecionar pub Inserir após campo Inserir antes do campo Recursivo - Remover etiquetas de parágrafo - Removerá quaisquer &lt;P&gt; do começo ao fim do texto Maiúscula Codificar URL Vai formatar caracteres especiais em URLs @@ -901,8 +887,6 @@ Para fechar a tarefa de tradução vá até os detalhes e clique no botão "Fech Nó Inicial do Conteúdo Nome de Usuário Permissões de usuário - Tipo de usuário - Tipos de usuários Escrevente diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/ru.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/ru.xml index 9d27efe6..5ec4775e 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/ru.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/ru.xml @@ -11,11 +11,14 @@ Изменить тип документа Копировать Создать + Создать шаблон содержимого + Создать группу Создать пакет Значение по умолчанию Удалить Отключить Очистить корзину + Включить Экспортировать Импортировать Импортировать пакет @@ -26,21 +29,48 @@ Публичный доступ Опубликовать Обновить узлы + Переименовать Опубликовать весь сайт Установить разрешения для страницы '%0%' - Выберите, куда переместить - В структуре документов ниже Восстановить Разрешения Откатить Направить на публикацию Направить на перевод + Задать группу + Задать права Сортировать - Направить на публикацию Перевести Скрыть + Разблокировать Обновить + + Содержимое + Администрирование + Структура + Другое + + + Разрешить доступ к назначению языков и доменов + Разрешить доступ к просмотру журнала истории узла + Разрешить доступ на просмотр узла + Разрешить доступ на смену типа документа для узла + Разрешить доступ к копированию узла + Разрешить доступ к созданию узлов + Разрешить доступ к удалению узлов + Разрешить доступ к перемещению узла + Разрешить доступ к установке и изменению правил публичного доступа для узла + Разрешить доступ к публикации узла + Разрешить доступ к изменению прав доступа к узлу + Разрешить доступ на возврат к предыдущим состояниям узла + Разрешить доступ к отправке узла на одобрение перед публикацией + Разрешить доступ к отправке узла на перевод данных + Разрешить доступ к изменению порядка сортировки узлов + Разрешить доступ к переводу данных узла + Разрешить доступ к сохранению узла + Разрешить доступ к созданию шаблона содержимого + Добавить новый домен Домен @@ -67,6 +97,15 @@ Наблюдать за + + Создать новый шаблон содержимого из '%0%' + Пустой + Выбрать шаблон содержимого + Шаблон содержимого создан + Создан шаблон содержимого из '%0%' + Другой шаблон содержимого с таким же названием уже существует + Шаблон содержимого — это предопределенный набор данных, который редактор может использовать для начального заполнения свойств при создании узлов содержимого + Завершено @@ -170,6 +209,7 @@ Альтернативный текст (необязательно) Элементы списка Нажмите для правки этого элемента + Начальный узел содержимого Создано пользователем Исходный автор Дата создания @@ -184,15 +224,19 @@ Документ опубликован Здесь еще нет элементов. В этом списке пока нет элементов. + Содержимое пока еще не добавлено + Участники пока еще не добавлены Ссылка на медиа-элементы Тип медиа-контента Группа участников - Член групп(ы) + Включен в группу(ы) Роль участника Тип участника + Вы уверены, что хотите удалить этот элемент? + Свойство '%0%' использует редактор '%1%', который не поддерживается для вложенного содержимого. Дата не указана - Заголовок ссылки - Не является членом групп(ы) + Заголовок страницы + Доступные группы Свойства Этот документ опубликован, но скрыт, потому что его родительский документ '%0%' не опубликован ВНИМАНИЕ: этот документ опубликован, но его нет в глобальном кэше (внутренняя ошибка - подробности в системном журнале) @@ -216,13 +260,15 @@ Обновлено Удалить файл Ссылка на документ + Добавить новое поле текста + Удалить это поле текста Композиции Вы не добавили ни одной вкладки Добавить вкладку Добавить новую вкладку - Унаследован от + Унаследовано от Добавить свойство Обязательная метка @@ -277,6 +323,7 @@ Где вы хотите создать новый %0% + Выберите тип документов, для которого нужно создать шаблон содержимого Создать в узле Новая папка Новый тип данных @@ -327,8 +374,12 @@ Открыть в новом окне? Свойства макроса Этот макрос не имеет редактируемых свойств + Заголовок ссылки Вставить Изменить разрешения для + Установить разрешения для + Установить права доступа к '%0%' для группы пользователей '%1%' + Выберите группу(ы) пользователей, для которых нужно установить разрешения Все элементы в корзине сейчас удаляются. Пожалуйста, не закрывайте это окно до окончания процесса удаления Корзина пуста Вы больше не сможете восстановить элементы, удаленные из корзины @@ -347,6 +398,7 @@ из нижеследующего списка. Список ограничивается идентификаторами, определенными в родительском шаблоне данного шаблона.]]> Кликните на изображении, чтобы увидеть полноразмерную версию Выберите элемент + Ссылка Просмотр элемента кэша Создать папку... Связать с оригиналом @@ -354,16 +406,23 @@ Самое дружелюбное сообщество Ссылка на страницу Открывать ссылку в новом окне или вкладке браузера - Ссылка на медиа-файл + Ссылка на медиа-элемент + Ссылка на файл Выбрать медиа + Выбрать начальный узел медиа-библиотеки Выбрать значок Выбрать элемент Выбрать ссылку Выбрать макрос Выбрать содержимое + Выбрать начальный узел содержимого Выбрать участника Выбрать группу участников + Выбрать узел + Выбрать разделы + Выбрать пользователей Это макрос без параметров + Нет макросов, доступных для вставки в редактор Провайдеры аутентификации Подробное сообщение об ошибке Трассировка стека @@ -372,6 +431,7 @@ Разорвать связь учетную запись Выбрать редактор + Выбрать образец Сопоставленные стили CSS Показать метку Ширина и высота + ВСЕ типы свойств и данные в свойствах документов, + использующие этот тип данных, будут удалены безвозвратно, подтвердите их удаление + Да, можно удалить + и все типы свойств и данные свойств, использующие этот тип данных + Выберите папку, чтобы переместить в нее + в структуре дерева ниже + был перемещен в папку Нет доступных элементов словаря @@ -476,6 +543,7 @@ Закрыть окно Примечание Подтвердить + Сохранять пропорции Сохранять пропорции Далее Копировать @@ -487,6 +555,7 @@ Удалено Удаляется... Дизайн + Словарь Размеры Вниз Скачать @@ -496,9 +565,12 @@ Email адрес Ошибка Найти + Начало + Группы Папка Высота Справка + Скрыть Иконка Импорт Внутренний отступ @@ -508,37 +580,44 @@ Выравнивание Название Язык + Конец Макет Загрузка БЛОКИРОВКА - Логин + Войти Выйти Выход Макрос Обязательно + Сообщение Больше Переместить Название Новый - Следующий + След Нет из + Выкл Ok Открыть + Вкл или + Сортировка по Пароль Путь Идентификатор контейнера Минуточку... - Предыдущий + Пред Свойства Email адрес для получения данных Корзина Ваша корзина пуста Осталось + Удалить Переименовать Обновить Обязательное + Получить Повторить Разрешения Поиск @@ -549,6 +628,7 @@ Показать страницу при отправке Размер Сортировать + Состояние Отправить Тип Что искать? @@ -556,7 +636,7 @@ Обновить Обновление Загрузить - URL + Интернет-ссылка Пользователь Имя пользователя Значение @@ -673,7 +753,7 @@ Разрешить HTTPS Устанавливает значение параметра 'umbracoSSL' в 'true' в секции 'appSettings' файла web.config. - Параметр 'umbracoUseSSL' в секции 'appSetting' файлf web.config теперь установлен в 'true', значения cookies будут промаркированы как безопасные. + Параметр 'umbracoUseSSL' в секции 'appSetting' файла web.config теперь установлен в 'true', значения cookies будут промаркированы как безопасные. Исправить Невозможно исправление по результату проверки значения на 'ShouldNotEqual'. @@ -722,6 +802,8 @@ %0%.]]> %0%.]]> +

Зафиксированы следующие результаты автоматической проверки состояния Umbraco по расписанию, запущенной на %0% в %1%:

%2%]]>
+ Результат проверки состояния Umbraco перейти к @@ -731,6 +813,10 @@ Сбросить + Задать рамку + Задайте рамке имя (алиас), а также ширину и высоту по-умолчанию + Сохранить рамку + Добавить новую рамку Программа установки не может установить подключение к базе данных. @@ -765,10 +851,6 @@ Пользователь по-умолчанию заблокирован или не имеет доступа к Umbraco!

Не будет предпринято никаких дальнейших действий. Нажмите кнопку Далее для продолжения.]]> Пароль пользователя по-умолчанию успешно изменен в процессе установки!

Нет надобности в каких-либо дальнейших действиях. Нажмите кнопку Далее для продолжения.]]> Пароль изменен! - Umbraco создает пользователя по-умолчанию с именем входа ('admin') и паролем ('default'). - ОЧЕНЬ ВАЖНО изменить пароль по-умолчанию на что-либо уникальное.

]]> -
Для начального обзора возможностей системы рекомендуем посмотреть ознакомительные видеоматериалы Далее (или модифицируя вручную ключ "UmbracoConfigurationStatus" в файле "web.config"), Вы принимаете лицензионное соглашение для данного программного обеспечения, расположенное ниже. Пожалуйста, обратите внимание, что установочный пакет Umbraco отвечает двум различным типам лицензий: лицензии MIT на программные продукты с открытым исходным кодом в части ядра системы и свободной лицензии Umbraco в части пользовательского интерфейса.]]> Система не установлена. @@ -799,7 +881,7 @@ Здесь можно узнать об этом подробнее) Вы также можете отложить установку "Runway" на более позднее время. Перейдите к разделу "Для разработчиков" и выберите пункт "Пакеты". + (Здесь можно узнать об этом подробнее) Вы также можете отложить установку "Runway" на более позднее время. Перейдите к разделу "Разработка" и выберите пункт "Пакеты". ]]> Вы только что установили чистую платформу Umbraco. Какой шаг будет следующим? "Runway" установлен @@ -876,7 +958,7 @@ Ссылка, по которой Вы попали сюда, неверна или устарела Umbraco: сброс пароля - Ваше имя пользователя для входа в панель администрирования Umbraco: %0%

Перейдите по этой ссылке для того, чтобы сбросить Ваш пароль, или скопируйте текст ссылки и вставьте в адресную строку своего браузера:

%1%

]]> +

Ваше имя пользователя для входа в панель администрирования Umbraco: %0%

Перейдите по этой ссылке для того, чтобы сбросить Ваш пароль, или скопируйте текст ссылки и вставьте в адресную строку своего браузера:

%1%

]]>
@@ -892,6 +974,7 @@ или нажмите сюда, чтобы выбрать файлы Разрешены только типы файлов: Максимально допустимый размер файла: + Начальный узел медиа Создать нового участника @@ -968,6 +1051,39 @@ Выберите файл пакета на своем компьютере, нажав на кнопку 'Обзор'
и указав на нужный файл. Пакеты Umbraco обычно являются архивами с расширением ".umb" или ".zip". ]]> + Перетащите сюда + или нажмите здесь для выбора файла пакета + Загрузить пакет + Установите локальный пакет из файла, расположенного на Вашем компьютере. Остерегайтесь устанавливать пакеты из непроверенных источников! + Загрузить еще один пакет + Отменить и загрузить другой пакет + Лицензия + Я принимаю + условия использования + Установить пакет + Завершить + Установленные пакеты + Ни одного пакета еще не установлено + 'Packages' наверху справа]]> + Поиск по пакетам + Результаты поиска по + Ничего не найдено по запросу + Пожалуйста, повторите поиск, уточнив запрос, или воспользуйтесь просмотром по категориям + Популярные + Недавно созданные + имеет на счету + очков кармы + Информация + Владелец + Соавторы + Создан + Текущая версия + Версия .NET + Загрузок + Нравится + Совместимость + Этот пакет совместим со следующими версиями Umbraco, по сообщениям участников сообщества. Полная совместимость не гарантируется для версий со значением ниже 100% + Внешние источники Автор Демонстрация Документация (описание) @@ -980,7 +1096,7 @@ Опции пакета Краткий обзор пакета Репозиторий пакета - Подтверждение деинсталляции + Подтверждение деинсталляции пакета Пакет деинсталлирован Указанный пакет успешно удален из системы Деинсталлировать пакет @@ -995,14 +1111,15 @@ История версий пакета Перейти на веб-сайт пакета Этот пакет уже установлен в системе - Этот пакет не может быть установлен, он требует наличия Umbraco версии как минимум %0% + Этот пакет не может быть установлен, он требует наличия Umbraco версии как минимум Удаление... Загрузка... Импорт... Установка... Перезапуск, подождите, пожалуйста... Все готово, сейчас браузер перезагрузит страницу, подождите, пожалуйста... - Пожалуйста, нажмите кнопку 'finish' для завершения установки и перезагрузки страницы. + Пожалуйста, нажмите кнопку 'Завершить' для завершения установки и перезагрузки страницы. + Загрузка пакета... Вставить, полностью сохранив форматирование (не рекомендуется) @@ -1012,10 +1129,13 @@ Подтвердите пароль - Укажите Ваш email + Укажите Ваш email... Укажите описание... + Укажите email... + Укажите сообщение... Укажите имя... Укажите теги (нажимайте Enter после каждого тега)... + Укажите имя пользователя... Укажите фильтр... Метка... Назовите %0%... @@ -1102,6 +1222,11 @@ Ссылка В новом окне + + Переименована + Укажите здесь новое название для папки + '%0%' была переименована в '%1%' + Текущая версия Красным отмечен текст, которого уже нет в последней версии, зеленым - текст, который добавлен]]> @@ -1119,7 +1244,7 @@ Смотритель Содержимое Курьер - Для Разработчиков + Разработка Формы Помощь Мастер конфигурирования Umbraco @@ -1184,8 +1309,8 @@ Порядок сортировки Дата создания Сортировка завершена - Перетаскивайте элементы на нужное место вверх или вниз для определения необходимого Вам порядка сортировки. Также можно щелкнуть по заголовкам столбцов, чтобы отсортировать все элементы сразу. -
Не закрывайте это окно до окончания процесса сортировки.]]>
+ Перетаскивайте элементы на нужное место вверх или вниз для определения необходимого Вам порядка сортировки. Также можно использовать заголовки столбцов, чтобы отсортировать все элементы сразу. + Процесс публикации был отменен установленным пакетом дополнений. @@ -1218,6 +1343,7 @@ Стиль CSS сохранен Шаблон сохранен Произошла ошибка при сохранении пользователя (проверьте журналы ошибок) + Группа пользователей сохранена Пользователь сохранен Тип пользователей сохранен Файл не сохранен @@ -1231,10 +1357,12 @@ Отменено Операция отменена установленным сторонним расширением или блоком кода Ошибка + Сохранено Представление не сохранено Произошла ошибка при сохранении файла Представление сохранено Представление сохранено без ошибок + Права доступа сохранены для Cкрипт Python не сохранен Cкрипт Python не может быть сохранен в связи с ошибками Cкрипт Python сохранен @@ -1254,6 +1382,23 @@ XSLT-документ не может быть сохранен, проверьте установки файловых разрешений XSLT-документ сохранен Ошибок в XSLT-документе нет + Удалено %0% групп пользователей + '%0%' была удалена + Активировано %0% пользователей + При активации пользователей произошла ошибка + Заблокировано %0% пользователей + При блокировке пользователей произошла ошибка + '%0%' сейчас активирован + При активации пользователя произошла ошибка + '%0%' сейчас заблокирован + При блокировке пользователя произошла ошибка + Группы пользователей установлены + Удалено %0% групп пользователей + '%0%' была удалена + Разблокировано %0% пользователей + При разблокировке пользователей произошла ошибка + '%0%' сейчас разблокирован + При разблокировке пользователя произошла ошибка Используется синтаксис селекторов CSS, например: h1, .redHeader, .blueTex @@ -1330,6 +1475,7 @@ Генератор запросов + Построить запрос элементов в результате, за Мне нужны @@ -1367,34 +1513,46 @@ Шаблон + Добавить поле замены + Поле замены + Добавить значение по-умолчанию + Значение по-умолчанию Альтернативное поле - Альтернативный текст + Текст по-умолчанию Регистр Выбрать поле Преобразовать переводы строк - Заменяет переводы строк на тэг html <br> + Да, преобразовывать + Заменяет переводы строк на тэг html 'br' Пользовательские Только дата Кодировка + Форматирование и кодировка Форматировать как дату + Форматировать значение как дату, или как дату и время, в соответствии с текущей культурой Кодировка HTML Заменяет спецсимволы эквивалентами в формате HTML Будет добавлено после поля Будет вставлено перед полем В нижнем регистре + Модификации при выводе -Не указано- + Пример результата Вставить после поля Вставить перед полем Рекурсивно - Удалить тэги параграфов - Удаляются тэги <p> в начале и в конце абзацев + Да, использовать рекурсию + Разделитель Стандартные В верхнем регистре Кодирование URL Форматирование специальных символов в URL Это значение будет использовано только если предыдущие поля пусты Это значение будет использовано только если первичное поле пусто - Дата и время. Разделитель: + Дата и время + + + символов осталось Задачи, назначенные Вам @@ -1448,6 +1606,8 @@ Аналитика Обзор кэша + Содержимое + Шаблоны содержимого Корзина Созданные пакеты Типы данных @@ -1458,6 +1618,7 @@ Языки Установить локальный пакет Макросы + Медиа-материалы Типы медиа-материалов Участники Группы участников @@ -1466,6 +1627,8 @@ Типы документов Пакеты дополнений Пакеты дополнений + Частичные представления + Файлы макросов Скрипты Python Типы связей Установить из репозитория @@ -1475,6 +1638,7 @@ Скрипты Стили CSS Шаблоны + Пользователи Файлы XSLT @@ -1484,24 +1648,53 @@ Во время проверки обновлений произошла ошибка. Пожалуйста, просмотрите журнал трассировки для получения дополнительной информации. + Доступ + На основании установленных групп и назначенных начальных узлов, пользователь имеет доступ к следующим узлам Администратор + Назначение доступа + Вернуться к пользователям Поле категории Изменить Изменить пароль Вы можете сменить свой пароль для доступа к административной панели Umbraco, заполнив нижеследующие поля и нажав на кнопку 'Изменить пароль' + Сменить аватар Подтверждение нового пароля Канал содержимого + Создать еще одного пользователя + Создан + Создать пользователя + Создавайте новых пользователей, которым нужен доступ к административной панели Umbraco. При создании пользователя для него генерируется новый первичный пароль, который нужно сообщить пользователю. Поле описания Отключить пользователя Тип документа Редактор Исключить поле + Неудачных попыток входа + К профилю пользователя + Добавьте пользователя в группу(ы) для задания прав доступа + Приглашение в панель администрирования Umbraco + +

Здравствуйте, %0%,

Вы были приглашены пользователем %1%, и Вам предоставлен доступ в панель администрирования Umbraco.

Сообщение от %1%: %2%

Перейдите по этой ссылке, чтобы принять приглашение.

Если Вы не имеете возможности перейти по ссылке, скопируйте нижеследующий текст ссылки и вставьте в адресную строку Вашего браузера.

%3%

]]> +
+ Пригласить еще одного пользователя + Пригласить пользователя + Пригласите новых пользователей, которым нужен доступ к административной панели Umbraco. Приглашенному будет направлено электронное письмо с инструкциями по доступу к Umbraco. Язык + Установите язык отображения интерфейса администрирования + Время последней блокировки + Время последнего входа + Пароль в последний раз менялся Имя входа (логин) - Начальный узел в Медиа-библиотеке + Начальный узел медиа-библиотеки + Можно ограничить доступ к медиа-библиотеке (какой-либо ее части), задав начальный узел + Начальные узлы медиа-библиотеки + Можно ограничить доступ к медиа-библиотеке (каким-либо ее частям), задав перечень начальных узлов Разделы Новый пароль Отключить доступ к административной панели Umbraco + пока еще не входил + пока не блокировался + Пароль не менялся Прежний пароль Пароль Ваш пароль доступа изменен! @@ -1515,15 +1708,35 @@ Заменить разрешения для дочерних документов Вы изменяете разрешения для следующих документов: Выберите документы для изменения их разрешений + Удалить аватар + Права доступа по-умолчанию + Атомарные права доступа + Можно установить права доступа к конкретным узлам + Профиль Сбросить пароль Поиск всех дочерних документов + Отправить приглашение Сессия истекает через - Начальный узел содержимого + Разделы, доступные пользователю + Начальный узел не задан + Начальные узлы не заданы + Начальный узел содержимого + Можно ограничить доступ к дереву содержимого (какой-либо его части), задав начальный узел + Начальные узлы содержимого + Можно ограничить доступ к дереву содержимого (каким-либо его частям), задав перечень начальных узлов + Был создан + Новый первичный пароль успешно сгенерирован. Для входа используйте пароль, приведенный ниже. Переводчик + Время последнего изменения Имя пользователя + Группа пользователей + Права доступа для группы + Группы пользователей + был приглашен + Новому пользователю было отправлено приглашение, в котором содержатся инструкции для входа в панель Umbraco. + Здравствуйте и добро пожаловать в Umbraco! Все будет готово в течении пары минут, нам лишь нужно задать Ваш пароль для входа и добавить аватар. + Загрузите изображение, это поможет другим пользователям идентифицировать Вас. Разрешения для пользователя - Тип пользователя - Типы пользователей Автор Ваша недавняя история Ваш профиль @@ -1535,5 +1748,13 @@ Валидация по формату Url ...или указать свои правила валидации Обязательно к заполнению + Задайте регулярное выражение + Необходимо выбрать как минимум + Возможно выбрать максимум + элементов + элементов + Неверный формат даты + Не является числом + неверный формат email-адреса diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/sv.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/sv.xml index dad2f2f7..3fba4304 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/sv.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/sv.xml @@ -12,7 +12,6 @@ Kopiera Skapa Skapa paket - Standardvärde Ta bort Avaktivera Töm papperskorgen @@ -32,7 +31,6 @@ Skicka för publicering Skicka för översättning Sortera - Skicka för publicering Översätt Avpublicera Uppdatera @@ -413,7 +411,7 @@ Vilken sida skall visas när formuläret är skickat Storlek Sortera - Submit + Skicka Skriv Skriv för att söka... Upp @@ -428,8 +426,17 @@ Bredd Titta på Ja - Reorder - I am done reordering + Sortera + Avsluta sortering + Förhandsvisning + Ändra lösenord + till + Listvy + Sparar... + nuvarande + Inbäddning + Hämta + valgt Bakgrundsfärg @@ -457,7 +464,6 @@ Standardanvändaren har avaktiverats eller har inte åtkomst till Umbraco!

Du behöver inte göra något ytterligare här. Klicka Next för att fortsätta.]]> Standardanvändarens lösenord har ändrats sedan installationen!

Du behöver inte göra något ytterligare här. Klicka Nästa för att fortsätta.]]> Lösenordet är ändrat! - Umbraco skapar en standardanvändare med login ('admin') och lösenordet ('default'). Det är viktigt att lösenordet ändras till något unikt.

Det här steget kommer kontrollera standardanvändarens lösenord och låta dig vet om det behöver ändras.

]]>
Få en flygande start, kolla på våra introduktionsvideor Genom att klicka på Nästa knappen (eller redigera UmbracoConfigurationStatus i web.config), accepterar du licensavtalet för den här mjukvaran som det är skrivet i rutan nedan. Observera att den här Umbracodistributionen består av två olika licensavtal, "the open source MIT license" för ramverket och "the Umbraco freeware license" som täcker användargränssnittet. Inte installerad än. @@ -638,8 +644,12 @@ Återställ + Definiera beskräning + Ge beskärningen ett alias och dess standardbredd och -höjd + spara beskärning + Lägg till ny beskärning - + Nuvarande version Röd text kommer inte att synas i den valda versionen. , Grön betyder att den har tillkommit]]> Dokumentet har återgått till en tidigare version @@ -693,7 +703,7 @@ Creation date Sortering klar Välj i vilken ordning du vill ha sidorna genom att dra dem upp eller ner i listan. Du kan också klicka på kolumnrubrikerna för att sortera grupper av sidor -
Stäng inte fönstret under tiden sidorna sorteras.]]>
+ Publiceringen avbröts av ett tredjepartstillägg @@ -820,8 +830,6 @@ Infoga efter fält Infoga före fält Rekursiv - Avlägsna stycke-taggar - Kommer att avlägsna alla &lt;P&gt; i början och slutet av texten Standardfält Versaler URL-koda @@ -931,8 +939,6 @@ Startnod i innehåll Användarens namn Användarrättigheter - Användartyp - Användartyper Skribent Din nuvarande historik Översättare diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/tr.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/tr.xml new file mode 100644 index 00000000..3bf32cc5 --- /dev/null +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/tr.xml @@ -0,0 +1,1131 @@ + + + + The Umbraco community + http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files + + + Kültür ve Hostnames + Denetim Trail + Düğüm Araştır + Belge Türü Değiştir + Kopya + Oluştur + Paket Oluştur + Sil + Devre Dışı Bırak + Geri Dönüşümü Boşat + Belge Türü Çıkart + Belge Türü Al + Paket Ekle + Tuval Düzele + Çıkış + Taşı + Bildirimler + Genel Erişim + Yayımla + Yayından Kaldır + Yeniden Yükle + Siteleri Yeniden Yayınla + Düzelt + İzinler + Rollback + Yayın için Gönder + Çeviri Gönder + Sırala + Yayına Gönder + Çevir + Güncelle + Varsayılan Değer + + + İzin reddedildi. + Yeni Domain ekle + kaldır + Geçersiz node. + Geçersiz domain biçimi. + Domain zaten eklenmiş. + Dil + Domain + Yeni domain '%0%' oluşturuldu + Domain '%0%' silindi + Domain '%0%' zaten atanmış + Domain '%0%' güncellendi + Geçerli domain düzenle + + etki /> bir düzey yollar desteklenir
+
+ Miras Al + Kültür + + veya üst düğümleri kültürünü devralır. Ayrıca
geçerli olacaktır + Geçerli düğümün , bir etki altında çok uygulanmadığı sürece .]]> +
+ Domainler + + + Görüntüleniyor + + + Seç + Geçerli klasörü seçin + Başka birşey yapın + Kalın + Paragraf girinti iptal + Form alanı ekle + Grafik başlık ekle + Html Düzenle + Paragraf girintisi + Yatık + Ortalı + Sola Yasla + Sağa Yasla + Link ekle + Yerel bağlantı ekle + Bulet listesi + Sayısal Liste + Macro ekle + Resim ekle + Düzenleme ilişkileri + Listeye Dön + Kaydet + Kaydet ve Yayınla + Kaydet ve Onay için gönder + Önizle + Önizleme kapalı, Atanmış şablon yok + Stili seçin + Stilleri Göster + Tablo Ekle + + + Seçilen içerik için belge türünü değiştirmek için , öncelikle bu konum için geçerli türleri listesinden seçim yapın. + Ardından onaylamak ve / veya yeni akım tip özellikleri haritalama değişiklik ve Kaydet'i tıklatın . + İçerik yeniden yayımlanmıştır . + Güncel Mülkiyet + Güncel tip + Bu konum için geçerli hiçbir alternatifi olduğu gibi belge türü , değiştirilemez . Seçilen içerik öğesinin ebeveyn altında izin verilir ve mevcut tüm alt içerik öğeleri altında oluşturulacak izin eğer bir alternatif geçerli olacaktır . + Belge Türü değiştirildi + harita Özellikleri + Mülkiyet Harita + Yeni Şablon + Yeni Tip + Hiçbiri + İçerik + Yeni Belge Tipi Seçiniz + Seçilen içeriğin belge türü başarıyla [ yeni tip ] değişti ve aşağıdaki özellikleri eşleştirilmiş edilmiştir : + için + Bir veya daha fazla özellikleri olarak mülkiyet haritalama tamamlayamadı birden fazla eşleme tanımlanmış var. + Bulunduğunuz yerin için geçerli Sadece alternatif türleri görüntülenir. + + + Yayımlandı + Bu sayfa hakkında + takma ad + ( nasıl telefon üzerinden resim anlatırsınız ) + Alternatif Linkler + Bu öğeyi düzenlemek için tıklayın + Tarafından yaratıldı + orijinal yazar + tarafından güncellendi + oluşturuldu + Bu belgenin oluşturulduğu tarih / zaman + Belge Türü + kurgu + en kaldır + Bu madde yayınlanmasından sonra değiştirildi + Bu öğe yayınlanmadı + Son yayınlanan + Listede gösterilecek öğe yok. + Medya Türü + Medya öğesinin bağlantı ( lar) + Üye Grubu + rol + Üye Türü + Hiçbir tarih seçildi + Sayfa başlığı + Özellikler + Ebeveyn ' %0% ' yayımlanmamış olduğu için bu belge yayınladı ama görünür değildir + Hata : Bu ​​belge yayınlandı ancak önbellek ( iç hata ) değil + yayınlamak + Yayın Durum + en Yayınla + en yayından + temizle tarihi + Sıralama güncellenir + Düğümlerini sıralamak için, sadece düğümleri sürükleyin veya sütun başlıkları birini tıklatın . Seçerken " shift " veya " kontrol " tuşunu basılı tutarak birden fazla düğüm seçebilirsiniz + istatistik + Başlık (isteğe bağlı) + Alternatif metin (isteğe bağlı) + tip + Yayından + Son düzenleme + Bu belgenin düzenlendiği tarih / zaman + Dosya(ları) kaldırın + Belgeye Bağlantı + Grubun Üyesi(leri) + Grubun bir üyesi değil + Çocuk öğeleri + hedef + + + Yüklemek için tıklayın + Burada açılan dosyaları ... + Medya Linki + + + Nerede yeni %0% yaratmak istiyorsun + Altında bir öğe oluşturun + Bir tür ve bir başlık seçin + "belge türleri".]]> + "ortam türleri".]]> + + + Web sitenizi tarayın + - gizle + CMS açılış değilse , bu siteden pop-up izin gerekebilir + Yeni bir pencere açtı + Tekrar başlat + ziyaret + hoşgeldiniz + + + isim + konak yönetin + Bu pencereyi kapatın + Silmek istediğine emin misin + Eğer devre dışı bırakmak istediğinizden emin misiniz + %0% öğe(lerin) silinmesi onaylamak için bu kutuyu kontrol edin + Emin misiniz? + Emin misiniz? + Kes + Düzenleme Sözlük Öğe + Dil Düzenleme + Yerel bağlantı ekleme + Karakter Ekle + Grafik başlığı ekleyin + Resim ekle + Link Ekle + Bir makro eklemek için tıklayın + Tablo Ekle + Son DÜzenleme + Bağlantı + İç Bağlantı: + Yerel bağlantıları kullanırken, bağlantının önündeki "# " insert + Yeni pencerede aç + Makro ayarları + Düzenleyebileceğiniz Bu makro herhangi bir özellikleri içermiyor + Yapıştır + İzinleri düzenle + Geri dönüşüm kutusu öğeleri şimdi siliniyor. Bu işlem gerçekleşirken bu pencereyi kapatın etmeyiniz + Geri dönüşüm kutusu artık boş + Öğeleri geri dönüşüm kutusu silindiğinde, onlar sonsuza kadar gitmiş olacak + regexlib.com's webcoder şu anda üzerinde hiçbir kontrole sahip bazı sorunları, yaşanıyor. Bu rahatsızlıktan dolayı çok üzgünüz.]]> + Bir düzenli ifade arama form alanına doğrulama ekleyin. Örnek: 'E-posta', zip code 'url' + Makro kaldır + Gerekli alan + Site yeniden indekslendi + Web sitesi yenilendi önbelleği olmuştur. Tüm içerik güncel artık yayımlamak. Tüm yayınlanmamış içeriği hala yayınlanmamış olmakla birlikte + Web sitesi önbelleği yenilenir olacaktır. Yayınlanmamış içerik yayınlanmamış kalacak ise tüm yayınlanan içerik, güncellenecektir. + Sütün sayısı + Satır sayısı + + Yer tutucu kimliği ayarla senin tutucu bir kimlik ayarlayarak Çocuğunuz şablonları bu şablon içine içerik enjekte edebilir, + Bir kullanarak bu kimliği bakarak <asp:content /> element.]]> + + + Yer tutucu kimliği seçin Aşağıdaki listeden. You can sadece +       Geçerli şablonun ustadan kimliği sitesinin seçin.]]> + + Tam boyutta görmek için resmin üzerine tıklayın + öğeyi seçin + Görünüm Önbellek Öğe + + + + %0%
' aşağıda
Sol taraftaki menüden 'diller' başlığı altında ek dil ekleyebilirsiniz + ]]> + + Kültür adı + + + Kullanıcı adınızı giriniz + Parolanızı giriniz + Ad %0%... + Adınızı girin... + Aramak için yazın... + Filtrelemek için yazın... + (Basın, her etiketinden sonra girin) etiket eklemek için yazın ... + + + Root'a izin ver + Bu Sadece İçerik Türleri İçerik ve Medya ağaçların kök düzeyinde oluşturulabilir kontrol + İzin alt düğüm çeşitleri + Belge Türü kompozisyonlar + Oluştur + Sekmesini sil + Tanım + Yeni sekme + Sekme + Küçük resim + Lüste görünümü etkinleştir + Bir sıralanabilir & göstermek için içerik öğesini yapılandırır; kendi çocuklarının aranabilir liste, çocuk ağacında gösterilen olmayacak + Liste görünümü + Etkin liste görünümü veri türü + Özel liste görünüm oluşturun + Özel liste görünümü kaldır + + + Ön değer ekle + Veritabanı veritürü + Mülkiyet editörü GUID + Mülkiyet editörü + Düğmeler + Gelişmiş ayarları etkinleştir... + Bağlam menüsünü etkinleştir + Eklenen görüntülerin maksimum varsayılan boyutu + İlgili stil + Etiketi göster + Yükseklik ve Genişlik + + + Verileriniz kaydedildi, ancak bu sayfayı yayınlamak için önce ilk düzeltmek için gereken bazı hatalar vardır: + Geçerli üyelik sağlayıcısı değişen şifreyi desteklemiyor (EnablePasswordRetrieval doğru olması gerekir) + %0% zaten var + Hatalar vardı: + Hatalar vardı: + Şifre %0% karakter uzunluğunda en az olması ve en az %1% non-alfa sayısal karakter (ler) içermelidir + %0% bir tamsayı olmalıdır + %1% sekmesinde %0% alan zorunludur + %0% zorunlu bir alandır + %0% - %1% bir doğru biçimde değil + %0% Bir doğru biçimde değil + + + Belirtilen dosya türü yönetici tarafından izin verilmeyen olmuştur + NOT! CodeMirror yapılandırma tarafından etkin olsa bile yeterince kararlı değil, çünkü Internet Explorer'da devre dışı bırakılır. + Yeni özellik tipine takma adını ve hem de doldurunuz! + Belirli bir dosya veya klasör için okuma / yazma erişimi olan bir sorun var + Error loading Partial View script (Dosya: %0%) + Error loading userControl '%0%' + Error loading customControl (Assembly: %0%, Type: '%1%') + Error loading MacroEngine script (Dosya: %0%) + "Error parsing XSLT file: %0% + "Error reading XSLT file: %0% + Lütfen bir başlık girin + Lütfen bir tür seçin + Orijinal boyutundan daha resmi büyütmek üzereyiz. Devam etmek istediğinizden emin misiniz? + Python komut dosyası hatası + O hatayı içerdiği için python komut dosyası, kaydedilmemiş (ler) + Silinen düğüm başlatın, lütfen yöneticinize başvurun + Tarzı değiştirmeden önce içerik işaretleyiniz + Henüz aktif stilleri + Birleştirmek istediğiniz iki hücre solundaki imleci Lütfen + Sen birleştirilmiş henüz bir hücreyi bölemezsiniz. + XSLT kaynak hatae + O hatayı içerdiği XSLT, kaydedilmemiş (ler)) + Bu özellik için kullanılan veri türüne sahip bir yapılandırma hatası var, veri türünü kontrol edin + + + Hakkında + Eylem + Eylemler + Ekle + Takma ad + Emin misiniz? + Sınır + tarafında + İptal + hücre marjı + Seçim + Kapat + Pencereyi kapat + Açıklama + Onayla + oranları sınırlamak + Devam et + Kopyala + Oluştur + Veritabanı + Tarih + Standart + Sil + Silindi + Siliniyor... + Dizayn + Boyutlar + Aşağı + İndir + Düzenle + Düzenlendi + Elemanları + E-Posta + Hata + Bul + Yükseklik + Yardım + İkon + İthalat + İç Marj + Ekle + Kur + Satır Uzunluğu + Dil + Düzen + Yükleniyor + Kilitli + Giriş yap + Oturum Kapat + Çıkış yap + Makro + Taşı + Daha + Ad + Yeni + Sonraki + Hayır + arasında + TAMAM + + veya + Parola + Yol + Yer tutucu ID + Bir dakika lütfen... + Önceki + Özellikler + Form verilerini almak için e-posta + Geridönüşüm kutusu + Kalan + Adını Değiştir + Yenile + Gerekli + Tekrar dene + İzinler + Arama + Sunucu + Göster + Gönder sayfasını göster + Boyut + Sırala + Tip + Aramak için yazın... + Yukarı + Güncelle + Yükselt + Yükle + URL + Kullanıcı + Kullanıcı adı + Değer + Görünüm + Hoşgeldiniz... + Genişlik + Evet + Klasör + Arama Sonuçları + + + Arka plan rengi + Kalın + Metin Rengi + Yazı + Metin + + + Sayfa + + + Yükleyici veritabanına bağlanamıyor. + Web.config dosyasını kaydedilemedi. El bağlantı dizesini değiştirin lütfen. + Web.config dosyasını kaydedilemedi. El bağlantı dizesini değiştirin lütfen.... + Veritabanı yapılandırması + + Kurulum için dğümeye basın %0% veritabanı + ]]> + + Sonraki Devam için.]]> + + Veritabanı bulunamadı! "Web.config" nin "bağlantı dizesinde" bilgi dosyası doğru olup olmadığını kontrol edin.

+

Devam etmek için, (Visual Studio veya sevdiğiniz metin editörü kullanarak) "web.config" dosyasını düzenlemek lütfen, altına gidin "UmbracoDbDSN" adlı anahtarı veritabanınız için bağlantı dizesini eklemek ve dosyayı kaydedin.

+

+ Tekrar dene. +
+ + Burada düzenleme web.config Hakkında Daha Fazla Bilgi.

]]> +
+ + + Gerekirse ISS'nize irtibata geçiniz. + Eğer yerel makine veya sunucu üzerinde yükleme ediyorsanız, sistem yöneticinizden bilgi gerekebilir.]]> + + + + CMS %0% için veritabanını yükseltme için yükseltme düğmesine basın +

+

+ Merak etmeyin - hiçbir içerik silinmeyecek ve her şey sonradan çalışmaya devam edecektir! +

+ ]]> +
+ + Sonraki işlem. ]]> + + next to continue the configuration wizard]]> + The Default users' password needs to be changed!]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> + The password is changed! + Get a great start, watch our introduction videos + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. + Not installed yet. + Affected files and folders + More information on setting up permissions for Umbraco here + You need to grant ASP.NET modify permissions to the following files/folders + + Your permission settings are almost perfect!

+ You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]> +
+ How to Resolve + Click here to read the text version + video tutorial on setting up folder permissions for Umbraco or read the text version.]]> + + Your permission settings might be an issue! +

+ You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]> +
+ + Your permission settings are not ready for Umbraco! +

+ In order to run Umbraco, you'll need to update your permission settings.]]> +
+ + Your permission settings are perfect!

+ You are ready to run Umbraco and install packages!]]> +
+ Resolving folder issue + Follow this link for more information on problems with ASP.NET and creating folders + Setting up folder permissions + + + + Baştan başlamak istiyorum + + learn how) + You can still choose to install Runway later on. Please go to the Developer section and choose Packages. + ]]> + + You've just set up a clean Umbraco platform. What do you want to do next? + Runway is installed + + + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules + ]]> + + Only recommended for experienced users + I want to start with a simple website + + + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, + but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, + Runway offers an easy foundation based on best practices to get you started faster than ever. + If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. +

+ + Included with Runway: Home page, Getting Started page, Installing Modules page.
+ Optional Modules: Top Navigation, Sitemap, Contact, Gallery. +
+ ]]> +
+ What is Runway + Step 1/5 Accept license + Step 2/5: Database configuration + Step 3/5: Validating File Permissions + Step 4/5: Check Umbraco security + Step 5/5: Umbraco is ready to get you started + Thank you for choosing Umbraco + + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + + + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + + Umbraco %0% is installed and ready for use + + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + + + started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]> +
+ + Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + + Connection to database failed. + Umbraco Version 3 + Umbraco Version 4 + Watch + + Umbraco %0% for a fresh install or upgrading from version 3.0. +

+ Press "next" to start the wizard.]]> +
+ + + Kültür Kodu + Kültür Adı + + + Sen boşta oldum ve çıkış otomatik olarak gerçekleşecek + İşinizi kaydetmek için şimdi Yenile + + + Pazar + Pazartesi + Salı + Çarşamba + Perşembe + Cuma + İçerik Yönetim Sistemi + Giriş Yapın + Oturum zaman aşımına uğradı + © 2015 - %0%
umbraco.com

]]>
+ + + Gösterge Paneli + Bölümler + İçerik + + + Choose page above... + %0% has been copied to %1% + Select where the document %0% should be copied to below + %0% has been moved to %1% + Select where the document %0% should be moved to below + has been selected as the root of your new content, click 'ok' below. + No node selected yet, please select a node in the list above before clicking 'ok' + The current node is not allowed under the chosen node because of its type + The current node cannot be moved to one of its subpages + The current node cannot exist at the root + The action isn't allowed since you have insufficient permissions on 1 or more child documents. + Relate copied items to original + + + Edit your notification for %0% + + + + + Hi %0%

+ +

This is an automated mail to inform you that the task '%1%' + has been performed on the page '%2%' + by the user '%3%' +

+ +

+

Update summary:

+ + %6% +
+

+ + + +

Have a nice day!

+ Cheers from the Umbraco robot +

]]> +
+ [%0%] Notification about %1% performed on %2% + Notifications + + + + + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. + ]]> + + Author + Demonstration + Documentation + Package meta data + Package name + Package doesn't contain any items + +
+ You can safely remove this from the system by clicking "uninstall package" below.]]> +
+ No upgrades available + Package options + Package readme + Package repository + Confirm uninstall + Package was uninstalled + The package was successfully uninstalled + Uninstall package + + + Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, + so uninstall with caution. If in doubt, contact the package author.]]> + + Download update from the repository + Upgrade package + Upgrade instructions + There's an upgrade available for this package. You can download it directly from the Umbraco package repository. + Package version + Package version history + View package website + + + Paste with full formatting (Not recommended) + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + Paste as raw text without any formatting at all + Paste, but remove formatting (Recommended) + + + Role based protection + using Umbraco's member groups.]]> + You need to create a membergroup before you can use role-based authentication. + Error Page + Used when people are logged on, but do not have access + Choose how to restrict access to this page + %0% is now protected + Protection removed from %0% + Login Page + Choose the page that contains the login form + Remove Protection + Select the pages that contain login form and error messages + Pick the roles who have access to this page + Set the login and password for this page + Single user protection + If you just want to setup simple protection using a single login and password + + + + + + + + + + + + + + + Include unpublished child pages + Publishing in progress - please wait... + %0% out of %1% pages have been published... + %0% has been published + %0% and subpages have been published + Publish %0% and all its subpages + + ok
to publish %0% and thereby making its content publicly available.

+ You can publish this page and all it's sub-pages by checking publish all children below. + ]]> + + + + You have not configured any approved colours + + + harici bağlantı gir + iç sayfa seç + Altyazı + Bağlantı + yeni pencere + Yeni altyazı gir + Bağlantı gir + + + Sıfırla + + + Varolan versiyon + Red text will not be shown in the selected version. , green means added]]> + Document has been rolled back + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view + Rollback to + Versiyon seç + Görünüm + + + Düzenleme komut dosyası + + + Kapıcı + İçerik + Kurya + Geliştirici + CMS Yapılandırma Sihirbazı + Medya + Üyeler + Haber Bültenleri + Ayarlar + İstatistik + Çeviri + Kullanıcılar + Yardım + Formlar + Analytics + + + git + Help topics for + Video chapters for + Kayadata + + + Varsayılan şablonu + Sözlük Anahtarı + To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + New Tab Title + Node type + Type + Stylesheet + Script + Stylesheet property + Tab + Tab Title + Tabs + Master Content Type enabled + This Content Type uses + as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself + No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Master Document Type + Create matching template + + + Sorting complete. + Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items + + + + Hata + Kullanıcı izniniz yeterli olmadığı için, işleminiz gerçekleştirilmedi. + İptal Edildi + İşleminiz 3.Parti yazılım tarafından iptal edildi. + Sayfa yayınlama 3.Parti yazılım tarafından iptal edildi. + Property type zaten bulunuyor + Property type oluşturuldu + DataType: %1%]]> + Propertytype silindi + Döküman Tipi kaydedildi + Tab oluşturuldu + Tab silindi + Tab id: %0% silindi + Stylesheet kaydedilmedi + Stylesheet kaydedildi + Stylesheet sorunsuz kaydedildi + Data tipi kaydedildi + Sözlük elemanı kaydedildi + Bağlı olduğu sayfa yayınlanmadığından dolayı işlem başarısız oldu. + İçerik yayınlandı + ve web sitesinde görülebilir. + İçeirk kaydedildi. + Remember to publish to make changes visible + Onaya gönder + Değişiklikler onaya gönderildi + Medya kaydedildi + Medya sorunsuz kaydedildi + Üye kaydedildi + Stylesheet Property kaydedildi + Stylesheet kaydedildi + Template kaydedildi + Kullanıcı kaydedilirken hata oluştu (log kontrol) + Kullanıcı kaydedildi + Kullanıcı tipi kaydedildi + Dosya kaydedilemedi + dosya kaydedilemedi. Lütfen dosya izinlerini kontrol edin + Dosya kaydedildi + Dosya sorunsuz kaydedildi + Dil kaydedildi + Python script kaydedilemedi + Python scripti oluşan hata nedeniyle kaydedilemedi + Python script kaydedildi + Python scriptinde hata buluanamadı + Template kaydedilmedi + Aynı isim ile 2 template bulunmadığından emin olun + Template kaydedildi + Template sorunsuz kaydedildi! + XSLT kaydedilmedi + XSLT hata içeriyor + XSLT kaydedilemedi, dosya izinlerini kontrol edin + XSLT kaydedildi + XSLT'de hata içermiyor + İçerik yayından kaldırıldı + Partial view kaydedildi + Partial view sorunsuz kaydedildi! + Partial view kaydedilmedi + Dosya kaydedilirken bir hata oluştu. + Script view kaydedildi + Script view sorunsuz kaydedildi! + Script view kaydedilmedi + Dosya kaydedilirken bir hata oluştu. + Dosya kaydedilirken bir hata oluştu. + + + CSS sözdizimi kullanımları. örneğin: h1, .redHeader, .blueTex + Stil dosyası düzenle + Stil dosyası özelliği düzenle + Name to identify the style property in the rich text editor + Ön izleme + Stiller + + + Edit template + Insert content area + Insert content area placeholder + Insert dictionary item + Insert Macro + Insert Umbraco page field + Master template + Quick Guide to Umbraco template tags + Template + + + Insert control + Choose a layout for the page + below and add your first element]]> + + Click to embed + Click to insert image + Image caption... + Write here... + Grid layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add grid layout + Adjust the layout by setting column widths and adding additional sections + + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + + + Alternative field + Alternative Text + Casing + Encoding + Choose field + Convert line breaks + Replaces line breaks with html-tag &lt;br&gt; + Custom Fields + Yes, Date only + Format as date + HTML encode + Will replace special characters by their HTML equivalent. + Will be inserted after the field value + Will be inserted before the field value + Lowercase + None + Insert after field + Insert before field + Recursive + Remove Paragraph tags + Will remove any &lt;P&gt; in the beginning and end of the text + Standard Fields + Uppercase + URL encode + Will format special characters in URLs + Will only be used when the field values above are empty + This field will only be used if the primary field is empty + Yes, with time. Separator: + + + Tasks assigned to you + + assigned to you. To see a detailed view including comments, click on "Details" or just the page name. + You can also download the page as XML directly by clicking the "Download Xml" link.
+ To close a translation task, please go to the Details view and click the "Close" button. + ]]> +
+ close task + Translation details + Download all translation tasks as XML + Download XML + Download XML DTD + Fields + Include subpages + + + + [%0%] Translation task for %1% + No translator users found. Please create a translator user before you start sending content to translation + Tasks created by you + + created by you. To see a detailed view including comments, + click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link. + To close a translation task, please go to the Details view and click the "Close" button. + ]]> + + The page '%0%' has been send to translation + Send the page '%0%' to translation + Atanan + Görev açıldı + Toplam kelime + Translate to + Çeviri tamamlandı. + You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. + Translation failed, the XML file might be corrupt + Çeviri opsiyonları + Çevirmen + Upload translation XML + + + Cache Browser + Çöp Kutusu + Oluşturulan Paketler + Data Tipleri + Sözlük + Kurulu paketler + Görünüm kur + Başlangıç kiti kur + Diller + Yerel paket yükle + Makrolar + Medya Tipleri + Üyeler + Üye Grupları + Roller + Üye Tipleri + Döküman Tipleri + Paketler + Paketler + Python Dosyaları + Depodan kurulum yap + Install Runway + Runway modülleri + Scripting Dosyaları + Scriptler + Stil dosyaları + Şablonlar + XSLT Dosyaları + Analitikler + + + Yeni bir güncelleme geldi + %0% hazır, indirimek için tıklayın + Sunucu bağlantısı yok + Güncelleme için kontrol hatası. Daha fazla bilgi için iz yığını gözden geçirin. + + + Yöneticiler + Kategori alanı + Şifreni değiştir + Yeni şifre + Yeni şifreyi onayla + Aşağıdaki formu doldurarak CMS Geri Office'i erişmek için parolanızı değiştirmeniz ve ' Şifre Değiştir ' düğmesine tıklayabilirsiniz + İçerik Kanalı + Açıklama alanı + Kullanıcıyı devre dışı bırak + Döküman Tipi + editör + Alıntı alan + Dil + Kullanıcı adı + Medya kütüphane düğüm başlatın + Bölümler + CMS Erişim devre dışı bırakma + Parola + Şifrenizi sıfırlayın + Şifreniz değiştirildi! + Yeni parolayı onaylayın + Yeni şifrenizi girin + Yeni şifre boş olamaz! + Mevcut Şifre + Geçersiz şifre + Yeni şifre ile teyit şifre arasında bir fark yoktu. Lütfen tekrar deneyin! + Teyit şifre yeni bir şifre eşleşmiyor ! + Alt düğümü izinlerini değiştirin + Şu anda sayfaları için izinleri değiştiriyorsunuz: + Onların izinlerini değiştirmek için sayfaları seçin + Tüm alt düğümlerde ara + içerikte düğüm başlat + Kullanıcı adı + Kullanıcı izinleri + Kullanıcı türü + Kullanıcı tipleri + Yazar + Çevirmen + Değiştir + Profiliniz + Son tarih + Oturum sona eriyor + + diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/zh.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/zh.xml index a71eb3b3..a2b235a2 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/zh.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/zh.xml @@ -29,24 +29,20 @@ 重新发布整站 恢复 为 %0%设置权限 - 选择移动目的地 - 到下列的树结构中 权限 回滚 提交至发布者 发送给翻译 排序 - 提交至发布者 翻译 更新 - 默认值 禁止访问 添加域名 移除 错误的节点 - 域名错误 + 域名错误 域名重复 语言 域名 @@ -55,13 +51,17 @@ 域名 '%0%' 已使用 域名 '%0%' 已更新 编辑当前域名 - + + https://www.example.com/、example.com/en、……使用 * 代表任意域名,
- 只需要设置语言部分即可。]]>
+ 只需要设置语言部分即可。]]> +
继承 语言 - - 也可以从父节点继承。]]> + + + 也可以从父节点继承。]]> + 域名 @@ -100,27 +100,29 @@ 显示样式 插入表格 生成模型 + 撤销 + 重做 要更改所选节点的文档类型,先在列表中选择合适的文档类型。 然后设置当前文档类型到新文档类型的各字段间的对应映射关系并保存。 - 内容已被重新发布 + 内容已被重新发布 当前属性 当前类型 不能改变文档类型,因为没有可替代的类型。 - 文档类型已更改 + 文档类型已更改 要映射的字段 映射字段 新模板 新类型 - + 内容 选择新的文档类型 选中文档的类型已被成功更改为[new type],以下字段被映射: 不能完成字段映射,因为存在一个字段映射至多字段的问题。 - 仅显示可作为替代的文档类型。 - + 仅显示可作为替代的文档类型。 + 已发布 关于本页 @@ -151,8 +153,8 @@ 属性 该文档不可见,因为其上级 '%0%' 未发布。 该文档已发布,但是没有更新至缓存(内部错误) - Could not get the url - This document is published but its url would collide with content %0% + 无法获取网址 + 此文档已发布,但其url将与内容相冲突 %0% 发布 发布状态 发布于 @@ -162,7 +164,7 @@ 拖拽项目或单击列头即可排序,可以按住Shift多选。 统计 标题(可选) - Alternative text (optional) + 备选 (可选) 类型 取消发布 最近编辑 @@ -175,11 +177,13 @@ 目标 这将转换到服务器上的以下时间: 这是什么意思?]]> + 添加其他文本框 + 删除此文本框 点击上传 将文件放在此处.. - 链接到媒体 + 链接到媒体 或单击此处选择文件 仅允许的文件类型为 最大文件大小为 @@ -197,6 +201,13 @@ 没有模板的文档类型 新建文件夹 新数据类型 + 新建 javascript 文件 + 新建空分部视图 + 新的分部视图宏 + 从代码段中新建分部视图 + 新的空分部视图宏 + 从代码段中新建分部视图宏 + 新的分部视图宏 (不带宏) 浏览您的网站 @@ -213,29 +224,24 @@ 您有未保存的更改 确实要离开此页吗?-您有未保存的更改 - + 完成 - 已删除 %0% 项 已删除 %0% 项 已删除 %0% 项,共 %1% 项 已删除 %0% 项,共 %1% 项 - 已发布 %0% 项 已发布 %0% 项 已发布 %0% 项,共 %1% 项 已发布 %0% 项,共 %1% 项 - 已取消发布 %0% 项 已取消发布 %0% 项 已取消发布 %0% 项,共 %1% 项 已取消发布 %0% 项,共 %1% 项 - 已移动 %0% 项 已移动 %0% 项 已移动 %0% 项,共 %1% 项 已移动 %0% 项,共 %1% 项 - 已复制 %0% 项 已复制 %0% 项 已复制 %0% 项,共 %1% 项 @@ -281,10 +287,14 @@ 网站缓存将会刷新,所有已发布的内容将会更新。 表格列数 表格行数 - 设置一个占位符id 您可以在子模板中通过该ID来插入内容, - 引用格式: <asp:content />。]]> - 选择一个 - 占位符id。]]> + + 设置一个占位符id 您可以在子模板中通过该ID来插入内容, + 引用格式: <asp:content />。]]> + + + 选择一个 + 占位符id。]]> + 点击图片查看完整大小 拾取项 查看缓存项 @@ -313,11 +323,16 @@ 取消链接您的 帐户 选择编辑器 + 帐号 + 选择编辑器 + 选择代码段 - + %0%’
您可以在左侧的“语言”中添加一种语言 - ]]>
+ ]]> + 语言名称 编辑字典项的键。 @@ -331,13 +346,14 @@ 输入您的密码 确认密码 命名 %0%... - 输入用户名... + 输入名称... 标签... 输入说明... 输入搜索关键字... 输入过滤词... 键入添加tags (在每个tag之后按 enter)... 输入您的电子邮件 + 您的用户名通常是您的电子邮件 允许在根目录 @@ -369,6 +385,13 @@ 关联的样式表 显示标签 宽和高 + 所有属性类型 & 属性数据 + 使用此数据类型将被永久删除, 请确认您还要删除这些 + 是, 删除 + 以及使用此数据类型的所有属性类型 & 属性数据 + 选择要移动的文件夹 + 在树结构下面 + 被移到下面 数据已保存,但是发布前您需要修正一些错误: @@ -427,6 +450,7 @@ 关闭窗口 备注 确认 + 约束 强制属性 继续 复制 @@ -438,6 +462,7 @@ 已删除 正在删除… 设计 + 字典 规格 下载 @@ -447,6 +472,7 @@ 邮箱 错误 查找文档 + 第一 帮助 图标 @@ -457,6 +483,7 @@ 无效 对齐 语言 + 最后 布局 加载中 锁定 @@ -484,18 +511,22 @@ 接收数据邮箱 回收站 保持状态中 + 移除 重命名 更新 必填 重试 权限 搜索 + 对不起, 我们找不到你要找的东西。 + 未添加任何项目 服务器 显示 在发送时预览 大小 排序 - 提交 + 提交 + 类型 输入内容开始查找… @@ -534,22 +565,26 @@ - 添加选项卡 - 添加属性 - 添加编辑器 - 添加模板 - 添加子节点 - 添加子项 - - 编辑数据类型 - - 导航节 - - 快捷方式 - 显示快捷方式 - - 切换列表视图 - 切换允许作为根 + 添加选项卡 + 添加属性 + 添加编辑器 + 添加模板 + 添加子节点 + 添加子项 + 编辑数据类型 + 导航节 + 快捷方式 + 显示快捷方式 + 切换列表视图 + 切换允许作为根 + 注释/取消注释行 + 移除行 + 向上复制行 + 向下复制行 + 向上移动行 + 向下移动线条 + 一般 + 编辑 @@ -568,26 +603,34 @@ 无法保存web.config文件,请手工修改。 发现数据库 数据库配置 - + 安装进行 %0% 数据库配置 - ]]> + ]]> + 下一步继续。]]> - 数据库未找到!请检查数据库连接串设置。

+ + 数据库未找到!请检查数据库连接串设置。

您可以自行编辑“web.config”文件,键名为 “UmbracoDbDSN”

当自行编辑后,单击重试按钮
。 如何编辑web.config

- ]]>
- + ]]> + + + 如有必要,请联系您的系统管理员。 - 如果您是本机安装,请使用管理员账号。]]> - + + + 点击更新来更新系统到 %0%

不用担心更新会丢失数据!

- ]]>
+ ]]> +
点击下一步继续。]]> 下一步继续]]> @@ -595,56 +638,62 @@ 默认账户已禁用或无权访问系统!

点击下一步继续。]]> 安装过程中默认用户密码已更改

点击下一步继续。]]> 密码已更改 - - 系统创建了一个默认用户(‘admin’)和默认密码(‘default’)。 - 现在密码是随机的。 -

-

- 该步骤建议您修改默认密码。 -

- ]]>
作为入门者,从视频教程开始吧! 点击下一步 (或在Web.config中自行修改UmbracoConfigurationStatus),意味着您接受上述许可协议。 安装失败。 受影响的文件和文件夹 此处查看更多信息 您需要对以下文件和文件夹授于ASP.NET用户修改权限 - 您当前的安全设置满足要求!

- 您可以毫无问题的运行系统,但您不能安装系统所推荐的扩展包的完整功能。]]>
+ + 您当前的安全设置满足要求!

+ 您可以毫无问题的运行系统,但您不能安装系统所推荐的扩展包的完整功能。]]> +
如何解决 点击阅读文字版 视频教程 ]]> - 您当前的安全设置有问题! + + 您当前的安全设置有问题!

- 您可以毫无问题的运行系统,但您不能新建文件夹、也不能安装系统所推荐的包的完整功能。 ]]>
- 您当前的安全设置不适合于系统! + 您可以毫无问题的运行系统,但您不能新建文件夹、也不能安装系统所推荐的包的完整功能。 ]]> + + + 您当前的安全设置不适合于系统!

- 您需要修改系统访问权限。]]>
- 您当前的权限设置正确!

- 您可以运行系统并安装其它扩展包!]]>
+ 您需要修改系统访问权限。]]> +
+ + 您当前的权限设置正确!

+ 您可以运行系统并安装其它扩展包!]]> +
解决文件夹问题 点此查看ASP.NET和创建文件夹的问题解决方案 设置文件夹权限 - + + ]]> + 我要从头开始 - + 如何操作?) 您也可以安装晚一些安装“Runway”。 - ]]> + ]]> + 您刚刚安装了一个干净的系统,要继续吗? “Runway”已安装 - + 这是我们推荐的模块,您也可以查看 全部模块 - ]]> + ]]> + 仅推荐高级用户使用 给我一个简单的网站 - + “Runway”是一个简单的,包含文件类型和模板的示例网站。安装程序会自动为您安装。 您可以自行编辑和删除之。 @@ -655,7 +704,8 @@ Runway: 主页, 开始页, 安装模块页.
可选模块: 顶部导航, 站点地图, 联系我们, 图库. - ]]>
+ ]]> + “Runway”是什么? 步骤 1/5:接受许可协议 步骤 2/5:数据库配置 @@ -663,24 +713,34 @@ 步骤 4/5:系统安全性 步骤 5/5:一切就绪,可以开始使用系统。 感谢选择我们的产品 - 浏览您的新站点 -您安装了“Runway”,那么来瞧瞧吧。]]> - 更多的帮助信息 -从社区获取帮助]]> + + 浏览您的新站点 +您安装了“Runway”,那么来瞧瞧吧。]]> + + + 更多的帮助信息 +从社区获取帮助]]> + 系统 %0% 安装完毕 - /web.config file 的 AppSetting 键 - UmbracoConfigurationStatus'%0%'。]]> + + /web.config file 的 AppSetting 键 + UmbracoConfigurationStatus'%0%'。]]> + 立即开始请点“运行系统”
如果您是新手, 您可以得到相当丰富的学习资源。]]>
- 运行系统 + + 运行系统 管理您的网站, 运行后台添加内容, -也可以添加模板和功能。]]> +也可以添加模板和功能。]]> + 无法连接到数据库。 系统版本 3 系统版本 4 观看 - +
-按 “下一步”进入向导。]]>
+按 “下一步”进入向导。]]> + 语言代码 @@ -735,7 +795,8 @@ 为 %0% 编写通知 - + - %0%:

+ ]]> +
+ + %0%:

您好!这是一封自动发送的邮件,告诉您任务'%1%' 已在'%2%' @@ -774,23 +837,61 @@

祝您愉快!

该信息由系统自动发送 -

]]>
+

]]> + 在 %2%,[%0%] 关于 %1% 的通告已执行。 通知 - + 选择 ".umb" 或者 ".zip" 文件 - ]]> + ]]> + + 拖入上传 + 或单击此处选择文件 + 上传包 + 通过从计算机中选择一个本地包来安装它。仅从您知道和信任的来源安装软件包 + 上传另一包 + 取消并上载另一个包 + 许可证 + 我接受 + 使用条款 + 安装包 + 完成 + 已安装的软件包 + 您没有安装任何软件包 + "程序包" 图标浏览可用的包]]> + 搜索包 + 结果为 + 我们找不到任何东西 + 请尝试搜索其他包或浏览类别 + 流行 + 新版本 + + karma 点 + 信息 + 所有者 + 贡献者 + 创建 + 当前版本 + .NET 版本 + 下载 + 喜欢 + 兼容性 + 此软件包与社区成员报告的 Umbraco 的以下版本兼容。报告100% 以下版本不能保证完全兼容 + 外部来源 作者 演示 文档 元数据 名称 扩展包不含任何项 -
- 点击下面的“卸载”,您可以安全的删除。]]>
+ +
+ 点击下面的“卸载”,您可以安全的删除。]]> +
无可用更新 选项 说明 @@ -799,9 +900,11 @@ 已卸载 扩展包卸载成功 卸载 - + + 注意: - 卸载包将导致所有依赖该包的东西失效,请确认。 ]]> + 卸载包将导致所有依赖该包的东西失效,请确认。 ]]> + 从程序库下载更新 更新扩展包 更新说明 @@ -818,6 +921,7 @@ 重启中, 请稍候... 所有完成后, 您的浏览器将立即刷新, 请稍候... 请单击 "完成" 以完成安装和重新加载页面。 + Uploading package... 带格式粘贴(不推荐) @@ -849,27 +953,37 @@ %0% 无法发布, 因为该项在计划发布中。 ]]> - + - + + + - + + + - + + + + ]]> + 包含未发布的子项 正在发布,请稍候… %0% 中的 %1% 页面已发布… %0% 已发布 %0% 及其子项已发布 发布 %0% 及其子项 - 确定 发布 %0%

+ + 确定 发布 %0%

要发布当前页和所有子页,请选中 全部发布 发布所有子页。 - ]]>
+ ]]> +
您没有配置任何认可的颜色 @@ -885,6 +999,10 @@ Reset + Define crop + Give the crop an alias and its default width and height + Save crop + Add new crop 当前版本 @@ -947,11 +1065,11 @@ 创建日期 排序完成。 上下拖拽项目或单击列头进行排序 -
请不要关闭窗口]]>
+ - 验证 - 在保存项之前必须修复验证错误 + 验证 + 在保存项之前必须修复验证错误 失败 用户权限不足, 无法完成操作 取消 @@ -1027,15 +1145,90 @@ 编辑模板 + 部分 插入内容区 插入内容占位符 + 插入 + 选择要插入到模板中的内容 插入字典项 + 字典项是可翻译的文本部分的占位符, 这使得为多语言网站创建设计变得容易。 插入宏 + + 宏是一个可配置的组件, 对于 + 设计的可重用部分, 在这里您需要提供参数的选项, + 如画廊、表格和列表。 + 插入页字段 + 显示当前页中指定字段的值, 其中有用于修改值或回退到替代值的选项。 + 分部视图 + + 分部视图是可以在另一个模板内呈现的单独的模板文件, + 它对于重用标记或将复杂的模板分离到单独的文件中非常重要。 + 母版 - 模板标签快速指南 + 无主模板 + 无主 + 呈现子模板 + + @RenderBody(). + ]]> + + 定义命名节 + + @section { ... }. 这可以呈现在 + 此模板的父级的特定区域, 请使用 @RenderSection. + ]]> + + 呈现命名节 + + @RenderSection(name). + 这将呈现子模板的一个区域, 它被包装在相应的 @section [名称] {...} 定义中. + ]]> + + 节名称 + 节是必需的 + + 如果强制, 子模板必须包含 @section 定义, 否则将显示错误。 + + 查询生成器 + 生成查询 + 返回的项, 在 + 我要 + 所有内容 + 类型 "%0%"的内容 + from + 我的网站 + where + and + is + is not + before + before (包含选定日期) + after + after (包含选定日期) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + Id + Name + Created Date + Last Updated Date + order by + ascending + descending 模板 + 选择内容类别 选择一项布局 @@ -1043,15 +1236,12 @@ 添加内容 丢弃内容 设置已应用 - 此处不允许有该内容 此处允许有该内容 - 点击嵌入 点击添加图片 图片说明... 在这里输入... - 网格布局 布局是网格编辑器的整体工作区域, 通常只需要一个或两个不同的布局 添加网络布局 @@ -1060,18 +1250,13 @@ 行是水平排列的预定义单元格 添加行配置 通过设置单元格宽度和添加其他单元格来调整行 - 网格布局中的总和列数 - 设置 配置编辑器可以更改的设置 - 样式 配置编辑器可以更改的样式 - 输入的 json 配置有效, 设置才可保存 - 允许所有的编辑器 允许所有行配置 设置为默认值 @@ -1081,89 +1266,81 @@ - - 组合 - 您没有添加任何选项卡 - 添加新选项卡 - 添加其他选项卡 - 继承自 - 添加属性 - 必需的标签 - - 启用列表视图 - 配置内容项以显示其子项的可排序和搜索列表, 这些子项将不会显示在树中 - - 允许的模板 - 选择允许在该类型的内容上使用哪些模板编辑器 - - 允许作为根 - 允许编辑器在内容树的根目录中创建此类型的内容 - 是 - 允许根中的此类型的内容 - - 允许的子节点类型 - 允许在该类型的内容下方创建指定类型的内容 - - 选择子节点 - - 从现有文档类型继承选项卡和属性。如果存在同名的选项卡, 则新选项卡将添加到当前文档类型或合并。 - 此内容类型在组合中使用, 因此不能自行组成。 - 没有可供组合使用的内容类型。 - - 可用编辑器 - 重用 - 编辑器设置 - - 配置 - - 是,删除 - - 被移动到下方 - 被复制到下面 - 选择要移动的文件夹 - 选择要复制的文件夹 - 在下面的树结构中 - - 所有文档类型 - 所有文档 - 所有媒体项目 - - 使用此文档类型将被永久删除, 请确认您还要删除这些文件。 - 使用此媒体类型将被永久删除, 请确认您也要删除这些。 - 使用此成员类型将被永久删除, 请确认您想要删除这些 - - 和所有使用此类型的文档 - 和所有使用此类型的媒体项目 - 和使用此类型的所有成员 - - 使用此编辑器将用新设置更新 - - 成员可编辑 - 显示成员配置文件 - + 组合 + 您没有添加任何选项卡 + 添加新选项卡 + 添加其他选项卡 + 继承自 + 添加属性 + 必需的标签 + 启用列表视图 + 配置内容项以显示其子项的可排序和搜索列表, 这些子项将不会显示在树中 + 允许的模板 + 选择允许在该类型的内容上使用哪些模板编辑器 + 允许作为根 + 允许编辑器在内容树的根目录中创建此类型的内容 + 是 - 允许根中的此类型的内容 + 允许的子节点类型 + 允许在该类型的内容下方创建指定类型的内容 + 选择子节点 + 从现有文档类型继承选项卡和属性。如果存在同名的选项卡, 则新选项卡将添加到当前文档类型或合并。 + 此内容类型在组合中使用, 因此不能自行组成。 + 没有可供组合使用的内容类型。 + 可用编辑器 + 重用 + 编辑器设置 + 配置 + 是,删除 + 被移动到下方 + 被复制到下面 + 选择要移动的文件夹 + 选择要复制的文件夹 + 在下面的树结构中 + 所有文档类型 + 所有文档 + 所有媒体项目 + 使用此文档类型将被永久删除, 请确认您还要删除这些文件。 + 使用此媒体类型将被永久删除, 请确认您也要删除这些。 + 使用此成员类型将被永久删除, 请确认您想要删除这些 + 和所有使用此类型的文档 + 和所有使用此类型的媒体项目 + 和使用此类型的所有成员 + 使用此编辑器将用新设置更新 + 成员可编辑 + 显示成员配置文件 + 添加后备字段 + 后备字段 + 添加默认值 + 默认值 替代字段 替代文本 大小写 + 编码 选取字段 转换换行符 + 是, 转换换行符 将换行符转化为&lt;br&gt; 自定义字段 是,仅日期 - 编码 + 格式和编码 格式化时间 + 根据活动区域性将该值设置为日期或日期。 HTML编码 将替换HTML中的特殊字符 将在字段值后插入 将在字段值前插入 小写 + 修改输出 + 输出示例 字段后插入 字段前插入 递归 - 移除段落符号 - 将移除&lt;P&gt;标签 + 是, 让它递归 + 分隔符 标准字段 大写 URL编码 @@ -1174,10 +1351,12 @@ 标记为您的任务 - 分配给您. 查看详情, 点击“详情”或页名。 + + 分配给您. 查看详情, 点击“详情”或页名。 如果需要XML格式,请点击“下载 XML”链接。
关闭翻译任务,请返回详细视图点击“关闭”按钮。 - ]]>
+ ]]> +
关闭任务 翻译详情 将翻译任务下载为xml @@ -1185,7 +1364,8 @@ 下载 XML DTD 字段 包含子页 - + + ]]> + [%0%]翻译任务:%1% 没有翻译员,请创建翻译员角色的用户。 您创建的任务 - 您创建的页面. 查看详情, 点击“详情” 或页名. + + 您创建的页面. 查看详情, 点击“详情” 或页名. 如果需要XML格式,请点击“下载 Xml”链接。
关闭翻译任务,请返回详细视图点击“关闭”按钮。 - ]]>
+ ]]> +
页面'%0%'已经发送给翻译 请选择内容应翻译成的语言 发送页面'%0%'以便翻译 @@ -1242,6 +1425,8 @@ 关系类型 扩展包 扩展包 + 分部视图 + 分部视图宏文件 Python文件 从在线程序库安装 安装Runway @@ -1252,8 +1437,6 @@ 模板 XSLT文件 分析 - 分部视图 - 分部视图宏文件 有可用更新 @@ -1297,8 +1480,6 @@ 默认打开内容项 用户名 用户权限 - 用户类型 - 用户类型 撰稿人 翻译人 更改 @@ -1307,95 +1488,94 @@ 会话过期于 - 验证 - 验证为电子邮件 - 验证为数字 - 验证为 url - ...或输入自定义验证 - 字段是强制性的 + 验证 + 验证为电子邮件 + 验证为数字 + 验证为 url + ...或输入自定义验证 + 字段是强制性的 + 输入正则表达式 + 您需要添加至少 + 你只能有 + + 选定的项 + 无效日期 + 不是一个数字 + 无效的电子邮件 - + Value is set to the recommended value: '%0%'. - Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. Found unexpected value '%0%' for '%2%' in configuration file '%3%'. - - Custom errors are set to '%0%'. - Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. - Custom errors successfully set to '%0%'. - - MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. - MacroErrors are now set to '%0%'. - - - Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. - Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). - Try Skip IIS Custom Errors successfully set to '%0%'. - - - File does not exist: '%0%'. - '%0%' in config file '%1%'.]]> - There was an error, check log for full error: %0%. - - Members - Total XML: %0%, Total: %1%, Total invalid: %2% - Media - Total XML: %0%, Total: %1%, Total invalid: %2% - Content - Total XML: %0%, Total published: %1%, Total invalid: %2% - - Your site certificate was marked as valid. - Certificate validation error: '%0%' - Error pinging the URL %0% - '%1%' - You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0% - - - Enable HTTPS - Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure. + + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. - Fix + + Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. + Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). + Try Skip IIS Custom Errors successfully set to '%0%'. + + + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + Members - Total XML: %0%, Total: %1%, Total invalid: %2% + Media - Total XML: %0%, Total: %1%, Total invalid: %2% + Content - Total XML: %0%, Total published: %1%, Total invalid: %2% + Your site certificate was marked as valid. + Certificate validation error: '%0%' + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0% + + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure. + Fix Cannot fix a check with a value comparison type of 'ShouldNotEqual'. Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. Value to fix check not provided. - - Debug compilation mode is disabled. - Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. - Debug compilation mode successfully disabled. - - Trace mode is disabled. - Trace mode is currently enabled. It is recommended to disable this setting before go live. - Trace mode successfully disabled. - + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. All folders have the correct permissions set. + 0: Comma delimitted list of failed folder paths + --> %0%.]]> %0%. If they aren't being written to no action need be taken.]]> - All files have the correct permissions set. + 0: Comma delimitted list of failed folder paths + --> %0%.]]> %0%. If they aren't being written to no action need be taken.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> Set Header in Config @@ -1404,16 +1584,14 @@ Could not update web.config file. Error: %0% + 0: Comma delimitted list of headers found + --> %0%.]]> No headers revealing information about the website technology were found. - In the Web.config file, system.net/mailsettings could not be found. In the Web.config file system.net/mailsettings section, the host is not configured. SMTP settings are configured correctly and the service is operating as expected. The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. - %0%.]]> %0%.]]> @@ -1434,4 +1612,7 @@ 现在已启用 url 跟踪程序。 启用 url 跟踪程序时出错, 可以在日志文件中找到更多信息。 + + 没有可供选择的词典项目 + diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/zh_tw.xml b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/zh_tw.xml index 53e893ea..24b9cc57 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/zh_tw.xml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Config/Lang/zh_tw.xml @@ -570,13 +570,6 @@ 預設使用者已經被暫停或沒有Umbraco的使用權!

不需更多的操作步驟。點選下一步繼續。]]> 安裝後預設使用者的密碼已經成功修改!

不需更多的操作步驟。點選下一步繼續。]]> 密碼已更改 - - Umbraco新增一預設使用者,名稱為('admin'),密碼為('default')。修改此密碼是十分重要的事情。

-

- 這個步驟會檢查預設使用者的密碼並在需要時建議修改。 -

- ]]>
作為入門者,從視頻教程開始吧! 點擊下一步 (或在Web.config中自行修改UmbracoConfigurationStatus),意味著您接受上述授權合約。 安裝失敗。 @@ -912,7 +905,7 @@ 增添時間 排序完成。 上下拖拽項目或按一下列頭進行排序 -
排序中請不要關閉視窗。]]>
+ 驗證 @@ -1232,8 +1225,6 @@ 預設打開內容項 用戶名 用戶許可權 - 用戶類型 - 用戶類型 撰稿人 翻譯者 改變 diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Developer/Macros/editMacro.aspx b/src/Umbraco.SampleSite.Website/Umbraco/Developer/Macros/editMacro.aspx index b2f6eef9..a968d0a3 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Developer/Macros/editMacro.aspx +++ b/src/Umbraco.SampleSite.Website/Umbraco/Developer/Macros/editMacro.aspx @@ -58,6 +58,9 @@ + + + diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Developer/Packages/editPackage.aspx b/src/Umbraco.SampleSite.Website/Umbraco/Developer/Packages/editPackage.aspx index 1f9258bf..6cfab403 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Developer/Packages/editPackage.aspx +++ b/src/Umbraco.SampleSite.Website/Umbraco/Developer/Packages/editPackage.aspx @@ -190,6 +190,7 @@ + @@ -203,7 +204,7 @@ during installation and uninstallation.
All actions are formed as a xml node, containing data for the action to be performed. - Package actions documentation

- - - - umbraco - <%=umbraco.ui.Text("editContentSendToPublish")%> - - - -

Republish <%=umbraco.ui.Text("editContentSendToPublishText")%>

-
- <%=umbraco.ui.Text("closewindow")%> - - +<%@ Page language="c#" MasterPageFile="../masterpages/umbracoDialog.Master" AutoEventWireup="True" Inherits="umbraco.dialogs.SendPublish" %> +<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> + + +
+ + + +
<%=umbraco.ui.Text("editContentSendToPublishText")%>
+ +
+
+ + +
diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Dialogs/rollBack.aspx b/src/Umbraco.SampleSite.Website/Umbraco/Dialogs/rollBack.aspx index e1abad94..3bf1c9a6 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Dialogs/rollBack.aspx +++ b/src/Umbraco.SampleSite.Website/Umbraco/Dialogs/rollBack.aspx @@ -1,4 +1,4 @@ -<%@ Page Language="c#" Codebehind="rollBack.aspx.cs" MasterPageFile="../masterpages/umbracoDialog.Master"AutoEventWireup="True" Inherits="umbraco.presentation.dialogs.rollBack" %> +<%@ Page Language="c#" MasterPageFile="../masterpages/umbracoDialog.Master" AutoEventWireup="True" Inherits="umbraco.presentation.dialogs.rollBack" %> <%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> <%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Install/Views/Index.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/Install/Views/Index.cshtml index 666f6a5b..bfbf5c41 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Install/Views/Index.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Install/Views/Index.cshtml @@ -1,4 +1,5 @@ -@using Umbraco.Web +@using Umbraco.Core.Configuration +@using Umbraco.Web @using Umbraco.Web.Install.Controllers @{ Layout = null; diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/app.dev.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/app.dev.js new file mode 100644 index 00000000..b1cb3859 --- /dev/null +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/app.dev.js @@ -0,0 +1,17 @@ +var app = angular.module('umbraco', [ + 'umbraco.filters', + 'umbraco.directives', + 'umbraco.resources', + 'umbraco.services', + 'umbraco.httpbackend', + 'ngCookies', + 'ngMobile', + 'ngSanitize', + 'tmh.dynamicLocale' +]); + +/* For Angular 1.4: we need to load in Route, animate and touch seperately + 'ngRoute', + 'ngAnimate', + 'ngTouch' +*/ \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/app.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/app.js index 36243abd..670ebf79 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/app.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/app.js @@ -39,7 +39,7 @@ angular.module("umbraco.viewcache", []) $delegate.get = function (url, config) { if (Umbraco.Sys.ServerVariables.application && url.startsWith("views/") && url.endsWith(".html")) { - var rnd = Umbraco.Sys.ServerVariables.application.version + "." + Umbraco.Sys.ServerVariables.application.cdf; + var rnd = Umbraco.Sys.ServerVariables.application.cacheBuster; var _op = (url.indexOf("?") > 0) ? "&" : "?"; url += _op + "umb__rnd=" + rnd; } diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/canvasdesigner.loader.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/canvasdesigner.loader.js index 59d5a4be..c5d2f700 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/canvasdesigner.loader.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/canvasdesigner.loader.js @@ -11,8 +11,8 @@ LazyLoad.js([ '../js/umbraco.security.js', '../ServerVariables', '../lib/spectrum/spectrum.js', - - '../js/canvasdesigner.panel.js', + '../js/umbraco.canvasdesigner.js', + '../js/canvasdesigner.panel.js' ], function () { jQuery(document).ready(function () { angular.bootstrap(document, ['Umbraco.canvasdesigner']); diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/init.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/init.js index 016c3301..631e3865 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/init.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/init.js @@ -1,13 +1,16 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['userService', '$log', '$rootScope', '$location', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', - function (userService, $log, $rootScope, $location, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService) { +app.run(['userService', '$log', '$rootScope', '$location', 'queryStrings', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', + function (userService, $log, $rootScope, $location, queryStrings, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService) { //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so // it cannot be static $.ajaxSetup({ beforeSend: function (xhr) { - xhr.setRequestHeader("X-XSRF-TOKEN", $cookies["XSRF-TOKEN"]); + xhr.setRequestHeader("X-UMB-XSRF-TOKEN", $cookies["UMB-XSRF-TOKEN"]); + if (queryStrings.getParams().umbDebug === "true" || queryStrings.getParams().umbdebug === "true") { + xhr.setRequestHeader("X-UMB-DEBUG", "true"); + } } }); diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/install.loader.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/install.loader.js index 869521ec..ead413ed 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/install.loader.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/install.loader.js @@ -1,17 +1,19 @@ -LazyLoad.js( [ - 'lib/jquery/jquery.min.js', - /* 1.1.5 */ - 'lib/angular/1.1.5/angular.min.js', - 'lib/angular/1.1.5/angular-cookies.min.js', - 'lib/angular/1.1.5/angular-mobile.min.js', - 'lib/angular/1.1.5/angular-mocks.js', - 'lib/angular/1.1.5/angular-sanitize.min.js', - 'lib/underscore/underscore-min.js', - 'js/umbraco.installer.js', - 'js/umbraco.directives.js' - ], function () { - jQuery(document).ready(function () { - angular.bootstrap(document, ['ngSanitize', 'umbraco.install', 'umbraco.directives.validation']); - }); - } +LazyLoad.js([ + 'lib/jquery/jquery.min.js', + /* 1.1.5 */ + 'lib/angular/1.1.5/angular.min.js', + 'lib/angular/1.1.5/angular-cookies.min.js', + 'lib/angular/1.1.5/angular-mobile.min.js', + 'lib/angular/1.1.5/angular-mocks.js', + 'lib/angular/1.1.5/angular-sanitize.min.js', + 'lib/underscore/underscore-min.js', + 'lib/angular/angular-ui-sortable.js', + 'js/installer.app.js', + 'js/umbraco.directives.js', + 'js/umbraco.installer.js' +], function () { + jQuery(document).ready(function () { + angular.bootstrap(document, ['umbraco']); + }); +} ); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/installer.app.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/installer.app.js new file mode 100644 index 00000000..05315493 --- /dev/null +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/installer.app.js @@ -0,0 +1,7 @@ +var app = angular.module('umbraco', [ + 'umbraco.directives', + 'umbraco.install', + 'ngCookies', + 'ngMobile', + 'ngSanitize' +]); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/loader.dev.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/loader.dev.js new file mode 100644 index 00000000..f842c187 --- /dev/null +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/loader.dev.js @@ -0,0 +1,49 @@ +LazyLoad.js( + [ + 'lib/jquery/jquery.min.js', + 'lib/jquery-ui/jquery-ui.min.js', + + + /* 1.1.5 */ + 'lib/angular/1.1.5/angular.min.js', + 'lib/angular/1.1.5/angular-cookies.min.js', + + 'lib/angular/1.1.5/angular-sanitize.min.js', + + 'lib/angular/angular-ui-sortable.js', + + 'lib/angular/1.1.5/angular-mocks.js', + 'lib/angular/1.1.5/angular-mobile.min.js', + 'lib/underscore/underscore-min.js', + + 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', + + 'lib/bootstrap/js/bootstrap.2.3.2.min.js', + 'lib/bootstrap-tabdrop/bootstrap-tabdrop.min.js', + 'lib/umbraco/Extensions.js', + + 'lib/umbraco/NamespaceManager.js', + 'lib/umbraco/LegacyUmbClientMgr.js', + 'lib/umbraco/LegacySpeechBubble.js', + + 'js/umbraco.servervariables.js', + 'js/app.dev.js', + 'js/umbraco.httpbackend.js', + 'js/umbraco.testing.js', + + 'js/umbraco.directives.js', + 'js/umbraco.filters.js', + 'js/umbraco.resources.js', + 'js/umbraco.services.js', + 'js/umbraco.security.js', + 'js/umbraco.controllers.js', + 'js/routes.js', + 'js/init.js' + ], + + function () { + jQuery(document).ready(function () { + angular.bootstrap(document, ['umbraco']); + }); + } +); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/routes.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/routes.js index c388ff58..26541200 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/routes.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/routes.js @@ -31,6 +31,12 @@ app.config(function ($routeProvider) { userService.getCurrentUser({ broadcastEvent: broadcast }).then(function (user) { //is auth, check if we allow or reject if (isRequired) { + + //This checks the current section and will force a redirect to 'content' as the default + if ($route.current.params.section.toLowerCase() === "default" || $route.current.params.section.toLowerCase() === "umbraco" || $route.current.params.section === "") { + $route.current.params.section = "content"; + } + // U4-5430, Benjamin Howarth // We need to change the current route params if the user only has access to a single section // To do this we need to grab the current user's allowed sections, then reject the promise with the correct path. @@ -98,14 +104,29 @@ app.config(function ($routeProvider) { resolve: doLogout() }) .when('/:section', { - templateUrl: function (rp) { - if (rp.section.toLowerCase() === "default" || rp.section.toLowerCase() === "umbraco" || rp.section === "") - { - rp.section = "content"; - } - - rp.url = "dashboard.aspx?app=" + rp.section; - return 'views/common/dashboard.html'; + + //This allows us to dynamically change the template for this route since you cannot inject services into the templateUrl method. + template: "
", + //This controller will execute for this route, then we can execute some code in order to set the template Url + controller: function ($scope, $route, $routeParams, $location, sectionService) { + + //We are going to check the currently loaded sections for the user and if the section we are navigating + //to has a custom route path we'll use that + sectionService.getSectionsForUser().then(function(sections) { + //find the one we're requesting + var found = _.find(sections, function(s) { + return s.alias === $routeParams.section; + }) + if (found && found.routePath) { + //there's a custom route path so redirect + $location.path(found.routePath); + } + else { + //there's no custom route path so continue as normal + $routeParams.url = "dashboard.aspx?app=" + $routeParams.section; + $scope.templateUrl = 'views/common/dashboard.html'; + } + }); }, resolve: canRoute(true) }) @@ -121,15 +142,11 @@ app.config(function ($routeProvider) { }) .when('/:section/:tree/:method', { templateUrl: function (rp) { + + //if there is no method registered for this then show the dashboard if (!rp.method) return "views/common/dashboard.html"; - - //NOTE: This current isn't utilized by anything but does open up some cool opportunities for - // us since we'll be able to have specialized views for individual sections which is something - // we've never had before. So could utilize this for a new dashboard model when we get native - // angular dashboards working. Perhaps a normal section dashboard would list out the registered - // dashboards (as tabs if we wanted) and each tab could actually be a route link to one of these views? - + return ('views/' + rp.tree + '/' + rp.method + '.html'); }, resolve: canRoute(true) diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.canvasdesigner.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.canvasdesigner.js new file mode 100644 index 00000000..9d60fb88 --- /dev/null +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.canvasdesigner.js @@ -0,0 +1,2450 @@ +(function () { + var app = angular.module('Umbraco.canvasdesigner', [ + 'colorpicker', + 'ui.slider', + 'umbraco.resources', + 'umbraco.services' + ]).controller('Umbraco.canvasdesignerController', function ($scope, $http, $window, $timeout, $location, dialogService) { + var isInit = $location.search().init; + if (isInit === 'true') { + //do not continue, this is the first load of this new window, if this is passed in it means it's been + //initialized by the content editor and then the content editor will actually re-load this window without + //this flag. This is a required trick to get around chrome popup mgr. We don't want to double load preview.aspx + //since that will double prepare the preview documents + return; + } + $scope.isOpen = false; + $scope.frameLoaded = false; + $scope.enableCanvasdesigner = 0; + $scope.googleFontFamilies = {}; + var pageId = $location.search().id; + $scope.pageId = pageId; + $scope.pageUrl = '../dialogs/Preview.aspx?id=' + pageId; + $scope.valueAreLoaded = false; + $scope.devices = [ + { + name: 'desktop', + css: 'desktop', + icon: 'icon-display', + title: 'Desktop' + }, + { + name: 'laptop - 1366px', + css: 'laptop border', + icon: 'icon-laptop', + title: 'Laptop' + }, + { + name: 'iPad portrait - 768px', + css: 'iPad-portrait border', + icon: 'icon-ipad', + title: 'Tablet portrait' + }, + { + name: 'iPad landscape - 1024px', + css: 'iPad-landscape border', + icon: 'icon-ipad flip', + title: 'Tablet landscape' + }, + { + name: 'smartphone portrait - 480px', + css: 'smartphone-portrait border', + icon: 'icon-iphone', + title: 'Smartphone portrait' + }, + { + name: 'smartphone landscape - 320px', + css: 'smartphone-landscape border', + icon: 'icon-iphone flip', + title: 'Smartphone landscape' + } + ]; + $scope.previewDevice = $scope.devices[0]; + var apiController = '../Api/Canvasdesigner/'; + /*****************************************************************************/ + /* Preview devices */ + /*****************************************************************************/ + // Set preview device + $scope.updatePreviewDevice = function (device) { + $scope.previewDevice = device; + }; + /*****************************************************************************/ + /* Exit Preview */ + /*****************************************************************************/ + $scope.exitPreview = function () { + window.top.location.href = '../endPreview.aspx?redir=%2f' + $scope.pageId; + }; + /*****************************************************************************/ + /* UI designer managment */ + /*****************************************************************************/ + // Update all Canvasdesigner config's values from data + var updateConfigValue = function (data) { + var fonts = []; + $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { + if (config.editors) { + $.each(config.editors, function (indexItem, item) { + /* try to get value */ + try { + if (item.values) { + angular.forEach(Object.keys(item.values), function (key, indexKey) { + if (key != '\'\'') { + var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); + var newValue = eval('data.' + propertyAlias.replace('@', '')); + if (newValue == '\'\'') { + newValue = ''; + } + item.values[key] = newValue; + } + }); + } + // TODO: special init for font family picker + if (item.type == 'googlefontpicker') { + if (item.values.fontType == 'google' && item.values.fontFamily + item.values.fontWeight && $.inArray(item.values.fontFamily + ':' + item.values.fontWeight, fonts) < 0) { + fonts.splice(0, 0, item.values.fontFamily + ':' + item.values.fontWeight); + } + } + } catch (err) { + console.info('Style parameter not found ' + item.alias); + } + }); + } + }); + // Load google font + $.each(fonts, function (indexFont, font) { + loadGoogleFont(font); + loadGoogleFontInFront(font); + }); + $scope.valueAreLoaded = true; + }; + // Load parameters from GetLessParameters and init data of the Canvasdesigner config + $scope.initCanvasdesigner = function () { + LazyLoad.js(['https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js']); + $http.get(apiController + 'Load', { params: { pageId: $scope.pageId } }).success(function (data) { + updateConfigValue(data); + $timeout(function () { + $scope.frameLoaded = true; + }, 200); + }); + }; + // Refresh all less parameters for every changes watching canvasdesignerModel + var refreshCanvasdesigner = function () { + var parameters = []; + if ($scope.canvasdesignerModel) { + angular.forEach($scope.canvasdesignerModel.configs, function (config, indexConfig) { + // Get currrent selected element + // TODO + //if ($scope.schemaFocus && angular.lowercase($scope.schemaFocus) == angular.lowercase(config.name)) { + // $scope.currentSelected = config.selector ? config.selector : config.schema; + //} + if (config.editors) { + angular.forEach(config.editors, function (item, indexItem) { + // Add new style + if (item.values) { + angular.forEach(Object.keys(item.values), function (key, indexKey) { + var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); + var value = eval('item.values.' + key); + parameters.splice(parameters.length + 1, 0, '\'@' + propertyAlias + '\':\'' + value + '\''); + }); + } + }); + } + }); + // Refresh page style + refreshFrontStyles(parameters); + // Refresh layout of selected element + //$timeout(function () { + $scope.positionSelectedHide(); + if ($scope.currentSelected) { + refreshOutlineSelected($scope.currentSelected); + } //}, 200); + } + }; + $scope.createStyle = function () { + $scope.saveLessParameters(false); + }; + $scope.saveStyle = function () { + $scope.saveLessParameters(true); + }; + // Save all parameter in CanvasdesignerParameters.less file + $scope.saveLessParameters = function (inherited) { + var parameters = []; + $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { + if (config.editors) { + $.each(config.editors, function (indexItem, item) { + if (item.values) { + angular.forEach(Object.keys(item.values), function (key, indexKey) { + var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); + var value = eval('item.values.' + key); + parameters.splice(parameters.length + 1, 0, '@' + propertyAlias + ':' + value + ';'); + }); + // TODO: special init for font family picker + if (item.type == 'googlefontpicker' && item.values.fontFamily) { + var variant = item.values.fontWeight != '' || item.values.fontStyle != '' ? ':' + item.values.fontWeight + item.values.fontStyle : ''; + var gimport = '@import url(\'https://fonts.googleapis.com/css?family=' + item.values.fontFamily + variant + '\');'; + if ($.inArray(gimport, parameters) < 0) { + parameters.splice(0, 0, gimport); + } + } + } + }); + } + }); + var resultParameters = { + parameters: parameters.join(''), + pageId: $scope.pageId, + inherited: inherited + }; + var transform = function (result) { + return $.param(result); + }; + $('.btn-default-save').attr('disabled', true); + $http.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; + $http.post(apiController + 'Save', resultParameters, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + transformRequest: transform + }).success(function (data) { + $('.btn-default-save').attr('disabled', false); + $('#speechbubble').fadeIn('slow').delay(5000).fadeOut('slow'); + }); + }; + // Delete current page Canvasdesigner + $scope.deleteCanvasdesigner = function () { + $('.btn-default-delete').attr('disabled', true); + $http.get(apiController + 'Delete', { params: { pageId: $scope.pageId } }).success(function (data) { + location.reload(); + }); + }; + /*****************************************************************************/ + /* Preset design */ + /*****************************************************************************/ + // Refresh with selected Canvasdesigner palette + $scope.refreshCanvasdesignerByPalette = function (palette) { + updateConfigValue(palette.data); + refreshCanvasdesigner(); + }; + // Hidden botton to make preset from the current settings + $scope.makePreset = function () { + var parameters = []; + $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { + if (config.editors) { + $.each(config.editors, function (indexItem, item) { + if (item.values) { + angular.forEach(Object.keys(item.values), function (key, indexKey) { + var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); + var value = eval('item.values.' + key); + var value = value != 0 && (value == undefined || value == '') ? '\'\'' : value; + parameters.splice(parameters.length + 1, 0, '"' + propertyAlias + '":' + ' "' + value + '"'); + }); + } + }); + } + }); + $('.btn-group').append(''); + }; + /*****************************************************************************/ + /* Panel managment */ + /*****************************************************************************/ + $scope.openPreviewDevice = function () { + $scope.showDevicesPreview = true; + $scope.closeIntelCanvasdesigner(); + }; + $scope.closePreviewDevice = function () { + $scope.showDevicesPreview = false; + if ($scope.showStyleEditor) { + $scope.openIntelCanvasdesigner(); + } + }; + $scope.openPalettePicker = function () { + $scope.showPalettePicker = true; + $scope.showStyleEditor = false; + $scope.closeIntelCanvasdesigner(); + }; + $scope.openStyleEditor = function () { + $scope.showStyleEditor = true; + $scope.showPalettePicker = false; + $scope.outlineSelectedHide(); + $scope.openIntelCanvasdesigner(); + }; + // Remove value from field + $scope.removeField = function (field) { + field.value = ''; + }; + // Check if category existe + $scope.hasEditor = function (editors, category) { + var result = false; + angular.forEach(editors, function (item, index) { + if (item.category == category) { + result = true; + } + }); + return result; + }; + $scope.closeFloatPanels = function () { + /* hack to hide color picker */ + $('.spectrumcolorpicker input').spectrum('hide'); + dialogService.close(); + $scope.showPalettePicker = false; + $scope.$apply(); + }; + $scope.clearHighlightedItems = function () { + $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { + config.highlighted = false; + }); + }; + $scope.setCurrentHighlighted = function (item) { + $scope.clearHighlightedItems(); + item.highlighted = true; + }; + $scope.setCurrentSelected = function (item) { + $scope.currentSelected = item; + $scope.clearSelectedCategory(); + refreshOutlineSelected($scope.currentSelected); + }; + /* Editor categories */ + $scope.getCategories = function (item) { + var propertyCategories = []; + $.each(item.editors, function (indexItem, editor) { + if (editor.category) { + if ($.inArray(editor.category, propertyCategories) < 0) { + propertyCategories.splice(propertyCategories.length + 1, 0, editor.category); + } + } + }); + return propertyCategories; + }; + $scope.setSelectedCategory = function (item) { + $scope.categoriesVisibility = $scope.categoriesVisibility || {}; + $scope.categoriesVisibility[item] = !$scope.categoriesVisibility[item]; + }; + $scope.clearSelectedCategory = function () { + $scope.categoriesVisibility = null; + }; + /*****************************************************************************/ + /* Call function into the front-end */ + /*****************************************************************************/ + var loadGoogleFontInFront = function (font) { + if (document.getElementById('resultFrame').contentWindow.getFont) + document.getElementById('resultFrame').contentWindow.getFont(font); + }; + var refreshFrontStyles = function (parameters) { + if (document.getElementById('resultFrame').contentWindow.refreshLayout) + document.getElementById('resultFrame').contentWindow.refreshLayout(parameters); + }; + var hideUmbracoPreviewBadge = function () { + var iframe = document.getElementById('resultFrame').contentWindow || document.getElementById('resultFrame').contentDocument; + if (iframe.document.getElementById('umbracoPreviewBadge')) + iframe.document.getElementById('umbracoPreviewBadge').style.display = 'none'; + }; + $scope.openIntelCanvasdesigner = function () { + if (document.getElementById('resultFrame').contentWindow.initIntelCanvasdesigner) + document.getElementById('resultFrame').contentWindow.initIntelCanvasdesigner($scope.canvasdesignerModel); + }; + $scope.closeIntelCanvasdesigner = function () { + if (document.getElementById('resultFrame').contentWindow.closeIntelCanvasdesigner) + document.getElementById('resultFrame').contentWindow.closeIntelCanvasdesigner($scope.canvasdesignerModel); + $scope.outlineSelectedHide(); + }; + var refreshOutlineSelected = function (config) { + var schema = config.selector ? config.selector : config.schema; + if (document.getElementById('resultFrame').contentWindow.refreshOutlineSelected) + document.getElementById('resultFrame').contentWindow.refreshOutlineSelected(schema); + }; + $scope.outlineSelectedHide = function () { + $scope.currentSelected = null; + if (document.getElementById('resultFrame').contentWindow.outlineSelectedHide) + document.getElementById('resultFrame').contentWindow.outlineSelectedHide(); + }; + $scope.refreshOutlinePosition = function (config) { + var schema = config.selector ? config.selector : config.schema; + if (document.getElementById('resultFrame').contentWindow.refreshOutlinePosition) + document.getElementById('resultFrame').contentWindow.refreshOutlinePosition(schema); + }; + $scope.positionSelectedHide = function () { + if (document.getElementById('resultFrame').contentWindow.outlinePositionHide) + document.getElementById('resultFrame').contentWindow.outlinePositionHide(); + }; + /*****************************************************************************/ + /* Google font loader, TODO: put together from directive, front and back */ + /*****************************************************************************/ + var webFontScriptLoaded = false; + var loadGoogleFont = function (font) { + if (!webFontScriptLoaded) { + $.getScript('https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js').done(function () { + webFontScriptLoaded = true; + // Recursively call once webfont script is available. + loadGoogleFont(font); + }).fail(function () { + console.log('error loading webfont'); + }); + } else { + WebFont.load({ + google: { families: [font] }, + loading: function () { + }, + active: function () { + }, + inactive: function () { + } + }); + } + }; + /*****************************************************************************/ + /* Init */ + /*****************************************************************************/ + // Preload of the google font + if ($scope.showStyleEditor) { + $http.get(apiController + 'GetGoogleFont').success(function (data) { + $scope.googleFontFamilies = data; + }); + } + // watch framLoaded, only if iframe page have enableCanvasdesigner() + $scope.$watch('enableCanvasdesigner', function () { + $timeout(function () { + if ($scope.enableCanvasdesigner > 0) { + $scope.$watch('ngRepeatFinished', function (ngRepeatFinishedEvent) { + $timeout(function () { + $scope.initCanvasdesigner(); + }, 200); + }); + $scope.$watch('canvasdesignerModel', function () { + refreshCanvasdesigner(); + }, true); + } + }, 100); + }, true); + }).directive('onFinishRenderFilters', function ($timeout) { + return { + restrict: 'A', + link: function (scope, element, attr) { + if (scope.$last === true) { + $timeout(function () { + scope.$emit('ngRepeatFinished'); + }); + } + } + }; + }).directive('iframeIsLoaded', function ($timeout) { + return { + restrict: 'A', + link: function (scope, element, attr) { + element.load(function () { + var iframe = element.context.contentWindow || element.context.contentDocument; + if (iframe && iframe.document.getElementById('umbracoPreviewBadge')) + iframe.document.getElementById('umbracoPreviewBadge').style.display = 'none'; + if (!document.getElementById('resultFrame').contentWindow.refreshLayout) { + scope.frameLoaded = true; + scope.$apply(); + } + }); + } + }; + }); + /*********************************************************************************************************/ + /* Global function and variable for panel/page com */ + /*********************************************************************************************************/ + var currentTarget = undefined; + var refreshLayout = function (parameters) { + // hide preview badget + $('#umbracoPreviewBadge').hide(); + var string = 'less.modifyVars({' + parameters.join(',') + '})'; + eval(string); + }; + /* Fonts loaded in the Canvasdesigner panel need to be loaded independently in + * the content iframe to allow live previewing. + */ + var webFontScriptLoaded = false; + var getFont = function (font) { + if (!webFontScriptLoaded) { + $.getScript('https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js').done(function () { + webFontScriptLoaded = true; + // Recursively call once webfont script is available. + getFont(font); + }).fail(function () { + console.log('error loading webfont'); + }); + } else { + WebFont.load({ + google: { families: [font] }, + loading: function () { + }, + active: function () { + }, + inactive: function () { + } + }); + } + }; + var closeIntelCanvasdesigner = function (canvasdesignerModel) { + if (canvasdesignerModel) { + $.each(canvasdesignerModel.configs, function (indexConfig, config) { + if (config.schema) { + $(config.schema).unbind(); + $(config.schema).removeAttr('canvasdesigner-over'); + } + }); + initBodyClickEvent(); + } + }; + var initBodyClickEvent = function () { + $('body').on('click', function () { + if (parent.iframeBodyClick) { + parent.iframeBodyClick(); + } + }); + }; + var initIntelCanvasdesigner = function (canvasdesignerModel) { + if (canvasdesignerModel) { + // Add canvasdesigner-over attr for each schema from config + $.each(canvasdesignerModel.configs, function (indexConfig, config) { + var schema = config.selector ? config.selector : config.schema; + if (schema) { + $(schema).attr('canvasdesigner-over', config.schema); + $(schema).attr('canvasdesigner-over-name', config.name); + $(schema).css('cursor', 'default'); + } + }); + // Outline canvasdesigner-over + $(document).mousemove(function (e) { + e.stopPropagation(); + var target = $(e.target); + while (target.length > 0 && (target.attr('canvasdesigner-over') == undefined || target.attr('canvasdesigner-over') == '')) { + target = target.parent(); + } + if (target.attr('canvasdesigner-over') != undefined && target.attr('canvasdesigner-over') != '') { + target.unbind(); + outlinePosition(target); + parent.onMouseoverCanvasdesignerItem(target.attr('canvasdesigner-over-name'), target); + target.click(function (e) { + e.stopPropagation(); + e.preventDefault(); + //console.info(target.attr('canvasdesigner-over')); + currentTarget = target; + outlineSelected(); + parent.onClickCanvasdesignerItem(target.attr('canvasdesigner-over'), target); + return false; + }); + } else { + outlinePositionHide(); + } + }); + } + }; + var refreshOutlinePosition = function (schema) { + outlinePosition($(schema)); + }; + var outlinePosition = function (oTarget) { + var target = oTarget; + if (target.length > 0 && target.attr('canvasdesigner-over') != undefined && target.attr('canvasdesigner-over') != '') { + var localname = target[0].localName; + var height = $(target).outerHeight(); + var width = $(target).outerWidth(); + var position = $(target).offset(); + var posY = position.top; + //$(window).scrollTop(); + var posX = position.left; + //+ $(window).scrollLeft(); + $('.canvasdesigner-overlay').css('display', 'block'); + $('.canvasdesigner-overlay').css('left', posX); + $('.canvasdesigner-overlay').css('top', posY); + $('.canvasdesigner-overlay').css('width', width + 'px'); + $('.canvasdesigner-overlay').css('height', height + 'px'); + //console.info("element select " + localname); + $('.canvasdesigner-overlay span').html(target.attr('canvasdesigner-over-name')); + } else { + outlinePositionHide(); //console.info("element not found select"); + } + }; + var refreshOutlineSelected = function (schema) { + outlineSelected($(schema)); + }; + var outlineSelected = function (oTarget) { + var target = currentTarget; + if (oTarget) { + currentTarget = oTarget; + target = oTarget; + } + if (target && target.length > 0 && target.attr('canvasdesigner-over') != undefined && target.attr('canvasdesigner-over') != '') { + var localname = target[0].localName; + var height = $(target).outerHeight(); + var width = $(target).outerWidth(); + var position = $(target).offset(); + var posY = position.top; + //$(window).scrollTop(); + var posX = position.left; + //+ $(window).scrollLeft(); + $('.canvasdesigner-overlay-selected').css('display', 'block'); + $('.canvasdesigner-overlay-selected').css('left', posX); + $('.canvasdesigner-overlay-selected').css('top', posY); + $('.canvasdesigner-overlay-selected').css('width', width + 'px'); + $('.canvasdesigner-overlay-selected').css('height', height + 'px'); + //console.info("element select " + localname); + $('.canvasdesigner-overlay-selected span').html(target.attr('canvasdesigner-over-name')); + } else { + outlinePositionHide(); //console.info("element not found select"); + } + }; + var outlinePositionHide = function () { + $('.canvasdesigner-overlay').css('display', 'none'); + }; + var outlineSelectedHide = function () { + currentTarget = undefined; + $('.canvasdesigner-overlay-selected').css('display', 'none'); + }; + var initCanvasdesignerPanel = function () { + $('link[data-title="canvasdesignerCss"]').attr('disabled', 'disabled'); + // First load the canvasdesigner config from file + if (!canvasdesignerConfig) { + console.info('canvasdesigner config not found'); + } + // Add canvasdesigner from HTML 5 data tags + $('[data-canvasdesigner]').each(function (index, value) { + var tagName = $(value).data('canvasdesigner') ? $(value).data('canvasdesigner') : $(value)[0].nodeName.toLowerCase(); + var tagSchema = $(value).data('schema') ? $(value).data('schema') : $(value)[0].nodeName.toLowerCase(); + var tagSelector = $(value).data('selector') ? $(value).data('selector') : tagSchema; + var tagEditors = $(value).data('editors'); + //JSON.parse(...); + canvasdesignerConfig.configs.splice(canvasdesignerConfig.configs.length, 0, { + name: tagName, + schema: tagSchema, + selector: tagSelector, + editors: tagEditors + }); + }); + // For each editor config create a composite alias + $.each(canvasdesignerConfig.configs, function (configIndex, config) { + if (config.editors) { + $.each(config.editors, function (editorIndex, editor) { + var clearSchema = config.schema.replace(/[^a-zA-Z0-9]+/g, '').toLowerCase(); + var clearEditor = JSON.stringify(editor).replace(/[^a-zA-Z0-9]+/g, '').toLowerCase(); + editor.alias = clearSchema + clearEditor; + }); + } + }); + // Create or update the less file + $.ajax({ + url: '/Umbraco/Api/CanvasDesigner/Init', + type: 'POST', + dataType: 'json', + error: function (err) { + alert(err.responseText); + }, + data: { + config: JSON.stringify(canvasdesignerConfig), + pageId: pageId + }, + success: function (data) { + // Add Less link in head + $('head').append(''); + css = $('head').children(':last'); + css.attr({ + rel: 'stylesheet/less', + type: 'text/css', + href: data + }); + //console.info("Less styles are loaded"); + // Init Less.js + $.getScript('/Umbraco/lib/Less/less-1.7.0.min.js', function (data, textStatus, jqxhr) { + // Init panel + if (parent.setFrameIsLoaded) { + parent.setFrameIsLoaded(canvasdesignerConfig, canvasdesignerPalette); + } + }); + } + }); + }; + $(function () { + if (parent.setFrameIsLoaded) { + // Overlay background-color: rgba(28, 203, 255, 0.05); + $('body').append(''); + $('body').append(''); + // Set event for any body click + initBodyClickEvent(); + // Init canvasdesigner panel + initCanvasdesignerPanel(); + } + }); + /*********************************************************************************************************/ + /* Global function and variable for panel/page com */ + /*********************************************************************************************************/ + /* Called for every canvasdesigner-over click */ + var onClickCanvasdesignerItem = function (schema) { + var scope = angular.element($('#canvasdesignerPanel')).scope(); + //if (scope.schemaFocus != schema.toLowerCase()) { + //var notFound = true; + $.each(scope.canvasdesignerModel.configs, function (indexConfig, config) { + if (config.schema && schema.toLowerCase() == config.schema.toLowerCase()) { + scope.currentSelected = config; + } + }); + //} + scope.clearSelectedCategory(); + scope.closeFloatPanels(); + scope.$apply(); + }; + /* Called for every canvasdesigner-over rollover */ + var onMouseoverCanvasdesignerItem = function (name) { + var scope = angular.element($('#canvasdesignerPanel')).scope(); + $.each(scope.canvasdesignerModel.configs, function (indexConfig, config) { + config.highlighted = false; + if (config.name && name.toLowerCase() == config.name.toLowerCase()) { + config.highlighted = true; + } + }); + scope.$apply(); + }; + /* Called when the iframe is first loaded */ + var setFrameIsLoaded = function (canvasdesignerConfig, canvasdesignerPalette) { + var scope = angular.element($('#canvasdesignerPanel')).scope(); + scope.canvasdesignerModel = canvasdesignerConfig; + scope.canvasdesignerPalette = canvasdesignerPalette; + scope.enableCanvasdesigner++; + scope.$apply(); + }; + /* Iframe body click */ + var iframeBodyClick = function () { + var scope = angular.element($('#canvasdesignerPanel')).scope(); + scope.closeFloatPanels(); + }; + /*********************************************************************************************************/ + /* Canvasdesigner setting panel config */ + /*********************************************************************************************************/ + var canvasdesignerConfig = { + configs: [ + { + name: 'Body', + schema: 'body', + selector: 'body', + editors: [ + { + type: 'background', + category: 'Color', + name: 'Background' + }, + { + type: 'color', + category: 'Font', + name: 'Font Color (main)', + css: 'color', + schema: 'body, h1, h2, h3, h4, h5, h6, h7, #nav li a' + }, + { + type: 'color', + category: 'Font', + name: 'Font Color (secondary)', + css: 'color', + schema: 'ul.meta, .byline' + }, + { + type: 'googlefontpicker', + category: 'Font', + name: 'Font Family', + css: 'color', + schema: 'body, h1, h2, h3, h4, h5, h6, h7, .byline, #nav, .button' + } + ] + }, + { + name: 'Nav', + schema: '#nav', + selector: 'nav', + editors: [ + { + type: 'background', + category: 'Color', + name: 'Background' + }, + { + type: 'border', + category: 'Color', + name: 'Border' + }, + { + type: 'color', + category: 'Nav', + name: 'Font Color', + css: 'color', + schema: '#nav li a' + }, + { + type: 'color', + category: 'Nav', + name: 'Font Color (hover / selected)', + css: 'color', + schema: '#nav li:hover a' + }, + { + type: 'color', + category: 'Nav', + name: 'Background Color (hover)', + css: 'background-color', + schema: '#nav li:hover a' + }, + { + type: 'color', + category: 'Nav', + name: 'Background Color (selected)', + css: 'background-color', + schema: '#nav li.current_page_item a' + }, + { + type: 'googlefontpicker', + category: 'Font', + name: 'Font familly' + } + ] + }, + { + name: 'Logo', + schema: '#header .logo div', + selector: '#header .logo div', + editors: [ + { + type: 'color', + category: 'Color', + name: 'Border color', + css: 'border-top-color', + schema: '#header .logo' + }, + { + type: 'padding', + category: 'Position', + name: 'Margin', + enable: [ + 'top', + 'bottom' + ], + schema: '#header' + } + ] + }, + { + name: 'h2', + schema: 'h2', + selector: 'h2 span', + editors: [ + { + type: 'color', + category: 'Color', + name: 'Border color', + css: 'border-top-color', + schema: 'h2.major' + }, + { + type: 'color', + category: 'Font', + name: 'Font color', + css: 'color' + } + ] + }, + { + name: 'h3', + schema: 'h3', + selector: 'h3', + editors: [{ + type: 'color', + category: 'Font', + name: 'Font color', + css: 'color' + }] + }, + { + name: 'Banner Title', + schema: '#banner h2', + selector: '#banner h2', + editors: [ + { + type: 'color', + category: 'Font', + name: 'Font color', + css: 'color' + }, + { + type: 'slider', + category: 'Font', + name: 'Font size', + css: 'font-size', + min: 18, + max: 100 + }, + { + type: 'margin', + category: 'Position', + name: 'Margin' + } + ] + }, + { + name: 'Banner Sub-title', + schema: '#banner .byline', + selector: '#banner .byline', + editors: [ + { + type: 'color', + category: 'Font', + name: 'Font color', + css: 'color' + }, + { + type: 'slider', + category: 'Font', + name: 'Font size', + css: 'font-size', + min: 18, + max: 100 + }, + { + type: 'margin', + category: 'Position', + name: 'Margin' + } + ] + }, + { + name: 'Banner', + schema: '#banner', + selector: '#banner', + editors: [{ + type: 'background', + category: 'Color', + name: 'Background', + css: 'color' + }] + }, + { + name: 'Banner-wrapper', + schema: '#banner-wrapper', + selector: '#banner-wrapper', + editors: [ + { + type: 'background', + category: 'Color', + name: 'Background' + }, + { + type: 'padding', + category: 'Position', + name: 'Padding', + enable: [ + 'top', + 'bottom' + ] + } + ] + }, + { + name: '#main-wrapper', + schema: '#main-wrapper', + selector: '#main-wrapper', + editors: [{ + type: 'border', + category: 'Styling', + name: 'Border', + enable: [ + 'top', + 'bottom' + ] + }] + }, + { + name: 'Image', + schema: '.image,.image img,.image:before', + selector: '.image', + editors: [{ + type: 'radius', + category: 'Styling', + name: 'Radius' + }] + }, + { + name: 'Button', + schema: '.button', + selector: '.button', + editors: [ + { + type: 'color', + category: 'Color', + name: 'Color', + css: 'color' + }, + { + type: 'color', + category: 'Color', + name: 'Background', + css: 'background' + }, + { + type: 'color', + category: 'Color', + name: 'Background Hover', + css: 'background', + schema: '.button:hover' + }, + { + type: 'radius', + category: 'Styling', + name: 'Radius' + } + ] + }, + { + name: 'Button Alt', + schema: '.button-alt', + selector: '.button-alt', + editors: [ + { + type: 'color', + category: 'Color', + name: 'Color', + css: 'color' + }, + { + type: 'color', + category: 'Color', + name: 'Background', + css: 'background' + }, + { + type: 'color', + category: 'Color', + name: 'Background Hover', + css: 'background', + schema: '.button-alt:hover' + } + ] + } + ] + }; + /*********************************************************************************************************/ + /* Canvasdesigner palette tab config */ + /*********************************************************************************************************/ + var canvasdesignerPalette = [ + { + name: 'Default', + color1: 'rgb(193, 202, 197)', + color2: 'rgb(231, 234, 232)', + color3: 'rgb(107, 119, 112)', + color4: 'rgb(227, 218, 168)', + color5: 'rgba(21, 28, 23, 0.95)', + data: { + 'widebodytypewidecategorydimensionnamelayout': 'wide', + 'imageorpatternbodytypebackgroundcategorycolornamebackground': '', + 'colorbodytypebackgroundcategorycolornamebackground': '', + 'colorbodytypecolorcategoryfontnamefontcolormaincsscolorschemabodyh1h2h3h4h5h6h7navlia': 'rgb(107, 119, 112)', + 'colorbodytypecolorcategoryfontnamefontcolorsecondarycsscolorschemaulmetabyline': 'rgb(193, 202, 197)', + 'fontfamilybodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': 'Open Sans Condensed', + 'fonttypebodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': 'google', + 'fontweightbodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': '700', + 'fontstylebodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': '', + 'imageorpatternnavtypebackgroundcategorycolornamebackground': '', + 'colornavtypebackgroundcategorycolornamebackground': '', + 'bordersizenavtypebordercategorycolornameborder': '', + 'bordercolornavtypebordercategorycolornameborder': '', + 'bordertypenavtypebordercategorycolornameborder': 'solid', + 'leftbordersizenavtypebordercategorycolornameborder': '', + 'leftbordercolornavtypebordercategorycolornameborder': '', + 'leftbordertypenavtypebordercategorycolornameborder': 'solid', + 'rightbordersizenavtypebordercategorycolornameborder': '', + 'rightbordercolornavtypebordercategorycolornameborder': '', + 'rightbordertypenavtypebordercategorycolornameborder': 'solid', + 'topbordersizenavtypebordercategorycolornameborder': '', + 'topbordercolornavtypebordercategorycolornameborder': '', + 'topbordertypenavtypebordercategorycolornameborder': 'solid', + 'bottombordersizenavtypebordercategorycolornameborder': '', + 'bottombordercolornavtypebordercategorycolornameborder': '', + 'bottombordertypenavtypebordercategorycolornameborder': 'solid', + 'colornavtypecolorcategorynavnamefontcolorcsscolorschemanavlia': 'rgb(107, 119, 112)', + 'colornavtypecolorcategorynavnamefontcolorhoverselectedcsscolorschemanavlihovera': 'rgb(255, 255, 255)', + 'colornavtypecolorcategorynavnamebackgroundcolorhovercssbackgroundcolorschemanavlihovera': 'rgb(193, 202, 197)', + 'colornavtypecolorcategorynavnamebackgroundcolorselectedcssbackgroundcolorschemanavlicurrentpageitema': 'rgb(227, 218, 168)', + 'fontfamilynavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fonttypenavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fontweightnavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fontstylenavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'colorheaderlogodivtypecolorcategorycolornamebordercolorcssbordertopcolorschemaheaderlogo': 'rgb(231, 234, 232)', + 'paddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'leftpaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'rightpaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'toppaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '172', + 'bottompaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '101', + 'colorh2typecolorcategorycolornamebordercolorcssbordertopcolorschemah2major': 'rgb(231, 234, 232)', + 'colorh2typecolorcategoryfontnamefontcolorcsscolor': '', + 'colorh3typecolorcategoryfontnamefontcolorcsscolor': '', + 'colorbannerh2typecolorcategoryfontnamefontcolorcsscolor': '', + 'sliderbannerh2typeslidercategoryfontnamefontsizecssfontsizemin18max100': '45', + 'marginvaluebannerh2typemargincategorypositionnamemargin': '', + 'leftmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'rightmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'topmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'bottommarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'colorbannerbylinetypecolorcategoryfontnamefontcolorcsscolor': '', + 'sliderbannerbylinetypeslidercategoryfontnamefontsizecssfontsizemin18max100': '22', + 'marginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'leftmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'rightmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'topmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'bottommarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'imageorpatternbannertypebackgroundcategorycolornamebackgroundcsscolor': '', + 'colorbannertypebackgroundcategorycolornamebackgroundcsscolor': 'rgba(21, 28, 23, 0.95)', + 'imageorpatternbannerwrappertypebackgroundcategorycolornamebackground': '', + 'colorbannerwrappertypebackgroundcategorycolornamebackground': '', + 'paddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'leftpaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'rightpaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'toppaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '123', + 'bottompaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '125', + 'bordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'bordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'bordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'leftbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'leftbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'leftbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'rightbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'rightbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'rightbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'topbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '32', + 'topbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': 'rgb(227, 218, 168)', + 'topbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'bottombordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '10', + 'bottombordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': 'rgb(193, 202, 197)', + 'bottombordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'radiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '8', + 'topleftradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '', + 'toprightradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '', + 'bottomleftradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '', + 'bottomrightradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '', + 'colorbuttontypecolorcategorycolornamecolorcsscolor': 'rgb(255, 255, 255)', + 'colorbuttontypecolorcategorycolornamebackgroundcssbackground': 'rgb(227, 218, 168)', + 'colorbuttontypecolorcategorycolornamebackgroundhovercssbackgroundschemabuttonhover': 'rgb(235, 227, 178)', + 'radiusvaluebuttontyperadiuscategorystylingnameradius': '7', + 'topleftradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'toprightradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'bottomleftradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'bottomrightradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'colorbuttonalttypecolorcategorycolornamecolorcsscolor': 'rgb(255, 255, 255)', + 'colorbuttonalttypecolorcategorycolornamebackgroundcssbackground': 'rgb(193, 202, 197)', + 'colorbuttonalttypecolorcategorycolornamebackgroundhovercssbackgroundschemabuttonalthover': 'rgb(204, 213, 208)' + } + }, + { + name: 'Blue Alternative', + color1: 'rgb(193, 202, 197)', + color2: 'rgb(231, 234, 232)', + color3: 'rgb(107, 119, 112)', + color4: 'rgb(68, 187, 204)', + color5: 'rgba(21, 28, 23, 0.95)', + data: { + 'widebodytypewidecategorydimensionnamelayout': 'wide', + 'imageorpatternbodytypebackgroundcategorycolornamebackground': '', + 'colorbodytypebackgroundcategorycolornamebackground': '', + 'colorbodytypecolorcategoryfontnamefontcolormaincsscolorschemabodyh1h2h3h4h5h6h7navlia': 'rgb(51, 68, 51)', + 'colorbodytypecolorcategoryfontnamefontcolorsecondarycsscolorschemaulmetabyline': 'rgb(68, 187, 204)', + 'fontfamilybodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': 'Alef', + 'fonttypebodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': 'google', + 'fontweightbodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': 'regular', + 'fontstylebodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': '', + 'imageorpatternnavtypebackgroundcategorycolornamebackground': '', + 'colornavtypebackgroundcategorycolornamebackground': '', + 'bordersizenavtypebordercategorycolornameborder': '', + 'bordercolornavtypebordercategorycolornameborder': '', + 'bordertypenavtypebordercategorycolornameborder': 'solid', + 'leftbordersizenavtypebordercategorycolornameborder': '', + 'leftbordercolornavtypebordercategorycolornameborder': '', + 'leftbordertypenavtypebordercategorycolornameborder': 'solid', + 'rightbordersizenavtypebordercategorycolornameborder': '', + 'rightbordercolornavtypebordercategorycolornameborder': '', + 'rightbordertypenavtypebordercategorycolornameborder': 'solid', + 'topbordersizenavtypebordercategorycolornameborder': '', + 'topbordercolornavtypebordercategorycolornameborder': '', + 'topbordertypenavtypebordercategorycolornameborder': 'solid', + 'bottombordersizenavtypebordercategorycolornameborder': '1', + 'bottombordercolornavtypebordercategorycolornameborder': 'rgba(0, 0, 0, 0.05)', + 'bottombordertypenavtypebordercategorycolornameborder': 'solid', + 'colornavtypecolorcategorynavnamefontcolorcsscolorschemanavlia': 'rgb(107, 119, 112)', + 'colornavtypecolorcategorynavnamefontcolorhoverselectedcsscolorschemanavlihovera': 'rgb(255, 255, 255)', + 'colornavtypecolorcategorynavnamebackgroundcolorhovercssbackgroundcolorschemanavlihovera': 'rgb(193, 202, 197)', + 'colornavtypecolorcategorynavnamebackgroundcolorselectedcssbackgroundcolorschemanavlicurrentpageitema': 'rgb(68, 187, 204)', + 'fontfamilynavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fonttypenavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fontweightnavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fontstylenavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'colorheaderlogodivtypecolorcategorycolornamebordercolorcssbordertopcolorschemaheaderlogo': 'rgb(231, 234, 232)', + 'paddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'leftpaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'rightpaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'toppaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '166', + 'bottompaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '91', + 'colorh2typecolorcategorycolornamebordercolorcssbordertopcolorschemah2major': 'rgb(231, 234, 232)', + 'colorh2typecolorcategoryfontnamefontcolorcsscolor': '', + 'colorh3typecolorcategoryfontnamefontcolorcsscolor': '', + 'colorbannerh2typecolorcategoryfontnamefontcolorcsscolor': '', + 'sliderbannerh2typeslidercategoryfontnamefontsizecssfontsizemin18max100': '45', + 'marginvaluebannerh2typemargincategorypositionnamemargin': '', + 'leftmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'rightmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'topmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'bottommarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'colorbannerbylinetypecolorcategoryfontnamefontcolorcsscolor': '', + 'sliderbannerbylinetypeslidercategoryfontnamefontsizecssfontsizemin18max100': '22', + 'marginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'leftmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'rightmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'topmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'bottommarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'imageorpatternbannertypebackgroundcategorycolornamebackgroundcsscolor': '', + 'colorbannertypebackgroundcategorycolornamebackgroundcsscolor': 'rgba(21, 28, 23, 0.95)', + 'imageorpatternbannerwrappertypebackgroundcategorycolornamebackground': '', + 'colorbannerwrappertypebackgroundcategorycolornamebackground': '', + 'paddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'leftpaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'rightpaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'toppaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '48', + 'bottompaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '55', + 'bordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'bordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'bordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'leftbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'leftbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'leftbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'rightbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'rightbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'rightbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'topbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '10', + 'topbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': 'rgb(68, 187, 204)', + 'topbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'bottombordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '10', + 'bottombordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': 'rgb(193, 202, 197)', + 'bottombordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'radiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '0', + 'topleftradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '20', + 'toprightradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '', + 'bottomleftradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '', + 'bottomrightradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '20', + 'colorbuttontypecolorcategorycolornamecolorcsscolor': 'rgb(255, 255, 255)', + 'colorbuttontypecolorcategorycolornamebackgroundcssbackground': 'rgb(68, 187, 204)', + 'colorbuttontypecolorcategorycolornamebackgroundhovercssbackgroundschemabuttonhover': 'rgb(133, 220, 232)', + 'radiusvaluebuttontyperadiuscategorystylingnameradius': '7', + 'topleftradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'toprightradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'bottomleftradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'bottomrightradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'colorbuttonalttypecolorcategorycolornamecolorcsscolor': 'rgb(255, 255, 255)', + 'colorbuttonalttypecolorcategorycolornamebackgroundcssbackground': 'rgb(193, 202, 197)', + 'colorbuttonalttypecolorcategorycolornamebackgroundhovercssbackgroundschemabuttonalthover': 'rgb(204, 213, 208)' + } + }, + { + name: 'Green safe', + color1: 'rgb(193, 202, 197)', + color2: 'rgb(240, 240, 240)', + color3: 'rgb(0, 153, 0)', + color4: 'rgb(0, 51, 0)', + color5: 'rgb(51, 51, 51)', + data: { + 'widebodytypewidecategorydimensionnamelayout': 'box', + 'imageorpatternbodytypebackgroundcategorycolornamebackground': '', + 'colorbodytypebackgroundcategorycolornamebackground': 'rgb(240, 240, 240)', + 'colorbodytypecolorcategoryfontnamefontcolormaincsscolorschemabodyh1h2h3h4h5h6h7navlia': 'rgb(85, 85, 85)', + 'colorbodytypecolorcategoryfontnamefontcolorsecondarycsscolorschemaulmetabyline': 'rgb(0, 153, 0)', + 'fontfamilybodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': 'Karma', + 'fonttypebodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': 'google', + 'fontweightbodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': '300', + 'fontstylebodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': '', + 'imageorpatternnavtypebackgroundcategorycolornamebackground': '', + 'colornavtypebackgroundcategorycolornamebackground': 'rgb(0, 51, 0)', + 'bordersizenavtypebordercategorycolornameborder': '', + 'bordercolornavtypebordercategorycolornameborder': '', + 'bordertypenavtypebordercategorycolornameborder': 'solid', + 'leftbordersizenavtypebordercategorycolornameborder': '', + 'leftbordercolornavtypebordercategorycolornameborder': '', + 'leftbordertypenavtypebordercategorycolornameborder': 'solid', + 'rightbordersizenavtypebordercategorycolornameborder': '', + 'rightbordercolornavtypebordercategorycolornameborder': '', + 'rightbordertypenavtypebordercategorycolornameborder': 'solid', + 'topbordersizenavtypebordercategorycolornameborder': '', + 'topbordercolornavtypebordercategorycolornameborder': '', + 'topbordertypenavtypebordercategorycolornameborder': 'solid', + 'bottombordersizenavtypebordercategorycolornameborder': '1', + 'bottombordercolornavtypebordercategorycolornameborder': 'rgba(0, 0, 0, 0.05)', + 'bottombordertypenavtypebordercategorycolornameborder': 'solid', + 'colornavtypecolorcategorynavnamefontcolorcsscolorschemanavlia': 'rgb(187, 187, 187)', + 'colornavtypecolorcategorynavnamefontcolorhoverselectedcsscolorschemanavlihovera': 'rgb(255, 255, 255)', + 'colornavtypecolorcategorynavnamebackgroundcolorhovercssbackgroundcolorschemanavlihovera': 'rgb(0, 153, 0)', + 'colornavtypecolorcategorynavnamebackgroundcolorselectedcssbackgroundcolorschemanavlicurrentpageitema': 'rgb(0, 153, 0)', + 'fontfamilynavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fonttypenavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fontweightnavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fontstylenavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'colorheaderlogodivtypecolorcategorycolornamebordercolorcssbordertopcolorschemaheaderlogo': 'rgb(231, 234, 232)', + 'paddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'leftpaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'rightpaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'toppaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '151', + 'bottompaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '57', + 'colorh2typecolorcategorycolornamebordercolorcssbordertopcolorschemah2major': 'rgb(231, 234, 232)', + 'colorh2typecolorcategoryfontnamefontcolorcsscolor': '', + 'colorh3typecolorcategoryfontnamefontcolorcsscolor': '', + 'colorbannerh2typecolorcategoryfontnamefontcolorcsscolor': 'rgb(0, 153, 0)', + 'sliderbannerh2typeslidercategoryfontnamefontsizecssfontsizemin18max100': '54', + 'marginvaluebannerh2typemargincategorypositionnamemargin': '', + 'leftmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'rightmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'topmarginvaluebannerh2typemargincategorypositionnamemargin': '33', + 'bottommarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'colorbannerbylinetypecolorcategoryfontnamefontcolorcsscolor': 'rgb(255, 255, 255)', + 'sliderbannerbylinetypeslidercategoryfontnamefontsizecssfontsizemin18max100': '26', + 'marginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'leftmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'rightmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'topmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'bottommarginvaluebannerbylinetypemargincategorypositionnamemargin': '30', + 'imageorpatternbannertypebackgroundcategorycolornamebackgroundcsscolor': '', + 'colorbannertypebackgroundcategorycolornamebackgroundcsscolor': 'rgb(51, 51, 51)', + 'imageorpatternbannerwrappertypebackgroundcategorycolornamebackground': '', + 'colorbannerwrappertypebackgroundcategorycolornamebackground': 'rgba(0, 153, 0, 0.15)', + 'paddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'leftpaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'rightpaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'toppaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '21', + 'bottompaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '21', + 'bordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'bordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'bordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'leftbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'leftbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'leftbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'rightbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'rightbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'rightbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'topbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '1', + 'topbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': 'rgba(68, 187, 204, 0)', + 'topbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'bottombordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '10', + 'bottombordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': 'rgb(193, 202, 197)', + 'bottombordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'radiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '8', + 'topleftradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '0', + 'toprightradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '0', + 'bottomleftradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '0', + 'bottomrightradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '0', + 'colorbuttontypecolorcategorycolornamecolorcsscolor': 'rgb(255, 255, 255)', + 'colorbuttontypecolorcategorycolornamebackgroundcssbackground': 'rgb(0, 51, 0)', + 'colorbuttontypecolorcategorycolornamebackgroundhovercssbackgroundschemabuttonhover': 'rgba(0, 51, 0, 0.62)', + 'radiusvaluebuttontyperadiuscategorystylingnameradius': '7', + 'topleftradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'toprightradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'bottomleftradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'bottomrightradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'colorbuttonalttypecolorcategorycolornamecolorcsscolor': 'rgb(255, 255, 255)', + 'colorbuttonalttypecolorcategorycolornamebackgroundcssbackground': 'rgb(193, 202, 197)', + 'colorbuttonalttypecolorcategorycolornamebackgroundhovercssbackgroundschemabuttonalthover': 'rgb(204, 213, 208)' + } + }, + { + name: 'Orange', + color1: 'rgb(193, 202, 197)', + color2: 'rgb(231, 234, 232)', + color3: 'rgb(230, 126, 34)', + color4: 'rgb(211, 84, 0)', + color5: 'rgb(51, 51, 51)', + data: { + 'widebodytypewidecategorydimensionnamelayout': 'wide', + 'imageorpatternbodytypebackgroundcategorycolornamebackground': '', + 'colorbodytypebackgroundcategorycolornamebackground': 'rgb(240, 240, 240)', + 'colorbodytypecolorcategoryfontnamefontcolormaincsscolorschemabodyh1h2h3h4h5h6h7navlia': 'rgb(85, 85, 85)', + 'colorbodytypecolorcategoryfontnamefontcolorsecondarycsscolorschemaulmetabyline': 'rgb(230, 126, 34)', + 'fontfamilybodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': 'Lato', + 'fonttypebodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': 'google', + 'fontweightbodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': '300', + 'fontstylebodytypegooglefontpickercategoryfontnamefontfamilycsscolorschemabodyh1h2h3h4h5h6h7bylinenavbutton': '', + 'imageorpatternnavtypebackgroundcategorycolornamebackground': '', + 'colornavtypebackgroundcategorycolornamebackground': 'rgb(51, 51, 51)', + 'bordersizenavtypebordercategorycolornameborder': '', + 'bordercolornavtypebordercategorycolornameborder': '', + 'bordertypenavtypebordercategorycolornameborder': 'solid', + 'leftbordersizenavtypebordercategorycolornameborder': '', + 'leftbordercolornavtypebordercategorycolornameborder': '', + 'leftbordertypenavtypebordercategorycolornameborder': 'solid', + 'rightbordersizenavtypebordercategorycolornameborder': '', + 'rightbordercolornavtypebordercategorycolornameborder': '', + 'rightbordertypenavtypebordercategorycolornameborder': 'solid', + 'topbordersizenavtypebordercategorycolornameborder': '', + 'topbordercolornavtypebordercategorycolornameborder': '', + 'topbordertypenavtypebordercategorycolornameborder': 'solid', + 'bottombordersizenavtypebordercategorycolornameborder': '1', + 'bottombordercolornavtypebordercategorycolornameborder': 'rgba(0, 0, 0, 0.05)', + 'bottombordertypenavtypebordercategorycolornameborder': 'solid', + 'colornavtypecolorcategorynavnamefontcolorcsscolorschemanavlia': 'rgb(187, 187, 187)', + 'colornavtypecolorcategorynavnamefontcolorhoverselectedcsscolorschemanavlihovera': 'rgb(255, 255, 255)', + 'colornavtypecolorcategorynavnamebackgroundcolorhovercssbackgroundcolorschemanavlihovera': 'rgb(181, 181, 181)', + 'colornavtypecolorcategorynavnamebackgroundcolorselectedcssbackgroundcolorschemanavlicurrentpageitema': 'rgb(211, 84, 0)', + 'fontfamilynavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fonttypenavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fontweightnavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'fontstylenavtypegooglefontpickercategoryfontnamefontfamilly': '', + 'colorheaderlogodivtypecolorcategorycolornamebordercolorcssbordertopcolorschemaheaderlogo': 'rgb(231, 234, 232)', + 'paddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'leftpaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'rightpaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '', + 'toppaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '151', + 'bottompaddingvalueheaderlogodivtypepaddingcategorypositionnamemarginenabletopbottomschemaheader': '57', + 'colorh2typecolorcategorycolornamebordercolorcssbordertopcolorschemah2major': 'rgb(231, 234, 232)', + 'colorh2typecolorcategoryfontnamefontcolorcsscolor': '', + 'colorh3typecolorcategoryfontnamefontcolorcsscolor': '', + 'colorbannerh2typecolorcategoryfontnamefontcolorcsscolor': '', + 'sliderbannerh2typeslidercategoryfontnamefontsizecssfontsizemin18max100': '54', + 'marginvaluebannerh2typemargincategorypositionnamemargin': '', + 'leftmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'rightmarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'topmarginvaluebannerh2typemargincategorypositionnamemargin': '33', + 'bottommarginvaluebannerh2typemargincategorypositionnamemargin': '', + 'colorbannerbylinetypecolorcategoryfontnamefontcolorcsscolor': 'rgb(225, 225, 225)', + 'sliderbannerbylinetypeslidercategoryfontnamefontsizecssfontsizemin18max100': '26', + 'marginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'leftmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'rightmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'topmarginvaluebannerbylinetypemargincategorypositionnamemargin': '', + 'bottommarginvaluebannerbylinetypemargincategorypositionnamemargin': '30', + 'imageorpatternbannertypebackgroundcategorycolornamebackgroundcsscolor': '', + 'colorbannertypebackgroundcategorycolornamebackgroundcsscolor': 'rgb(230, 126, 34)', + 'imageorpatternbannerwrappertypebackgroundcategorycolornamebackground': '', + 'colorbannerwrappertypebackgroundcategorycolornamebackground': 'rgb(255, 255, 255)', + 'paddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'leftpaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'rightpaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '', + 'toppaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '21', + 'bottompaddingvaluebannerwrappertypepaddingcategorypositionnamepaddingenabletopbottom': '21', + 'bordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'bordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'bordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'leftbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'leftbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'leftbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'rightbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'rightbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': '', + 'rightbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'topbordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '1', + 'topbordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': 'rgba(68, 187, 204, 0)', + 'topbordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'bottombordersizemainwrappertypebordercategorystylingnameborderenabletopbottom': '10', + 'bottombordercolormainwrappertypebordercategorystylingnameborderenabletopbottom': 'rgb(193, 202, 197)', + 'bottombordertypemainwrappertypebordercategorystylingnameborderenabletopbottom': 'solid', + 'radiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '8', + 'topleftradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '0', + 'toprightradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '0', + 'bottomleftradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '0', + 'bottomrightradiusvalueimageimageimgimagebeforetyperadiuscategorystylingnameradius': '0', + 'colorbuttontypecolorcategorycolornamecolorcsscolor': 'rgb(255, 255, 255)', + 'colorbuttontypecolorcategorycolornamebackgroundcssbackground': 'rgb(230, 126, 34)', + 'colorbuttontypecolorcategorycolornamebackgroundhovercssbackgroundschemabuttonhover': 'rgba(230, 126, 34, 0.55)', + 'radiusvaluebuttontyperadiuscategorystylingnameradius': '7', + 'topleftradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'toprightradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'bottomleftradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'bottomrightradiusvaluebuttontyperadiuscategorystylingnameradius': '', + 'colorbuttonalttypecolorcategorycolornamecolorcsscolor': 'rgb(255, 255, 255)', + 'colorbuttonalttypecolorcategorycolornamebackgroundcssbackground': 'rgb(193, 202, 197)', + 'colorbuttonalttypecolorcategorycolornamebackgroundhovercssbackgroundschemabuttonalthover': 'rgb(204, 213, 208)' + } + } + ]; + /*********************************************************************************************************/ + /* Background editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.background', function ($scope, dialogService) { + if (!$scope.item.values) { + $scope.item.values = { + imageorpattern: '', + color: '' + }; + } + $scope.open = function (field) { + var config = { + template: 'mediaPickerModal.html', + change: function (data) { + $scope.item.values.imageorpattern = data; + }, + callback: function (data) { + $scope.item.values.imageorpattern = data; + }, + cancel: function (data) { + $scope.item.values.imageorpattern = data; + }, + dialogData: $scope.googleFontFamilies, + dialogItem: $scope.item.values.imageorpattern + }; + dialogService.open(config); + }; + }).controller('canvasdesigner.mediaPickerModal', function ($scope, $http, mediaResource, umbRequestHelper, entityResource, mediaHelper) { + if (mediaHelper && mediaHelper.registerFileResolver) { + mediaHelper.registerFileResolver('Umbraco.UploadField', function (property, entity, thumbnail) { + if (thumbnail) { + if (mediaHelper.detectIfImageByExtension(property.value)) { + var thumbnailUrl = umbRequestHelper.getApiUrl('imagesApiBaseUrl', 'GetBigThumbnail', [{ originalImagePath: property.value }]); + return thumbnailUrl; + } else { + return null; + } + } else { + return property.value; + } + }); + } + var modalFieldvalue = $scope.dialogItem; + $scope.currentFolder = {}; + $scope.currentFolder.children = []; + $scope.currentPath = []; + $scope.startNodeId = -1; + $scope.options = { + url: umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'PostAddFile'), + formData: { currentFolder: $scope.startNodeId } + }; + //preload selected item + $scope.selectedMedia = undefined; + $scope.submitFolder = function (e) { + if (e.keyCode === 13) { + e.preventDefault(); + $scope.$parent.data.showFolderInput = false; + if ($scope.$parent.data.newFolder && $scope.$parent.data.newFolder != '') { + mediaResource.addFolder($scope.$parent.data.newFolder, $scope.currentFolder.id).then(function (data) { + $scope.$parent.data.newFolder = undefined; + $scope.gotoFolder(data); + }); + } + } + }; + $scope.gotoFolder = function (folder) { + if (!folder) { + folder = { + id: $scope.startNodeId, + name: 'Media', + icon: 'icon-folder' + }; + } + if (folder.id > 0) { + var matches = _.filter($scope.currentPath, function (value, index) { + if (value.id == folder.id) { + value.indexInPath = index; + return value; + } + }); + if (matches && matches.length > 0) { + $scope.currentPath = $scope.currentPath.slice(0, matches[0].indexInPath + 1); + } else { + $scope.currentPath.push(folder); + } + } else { + $scope.currentPath = []; + } + //mediaResource.rootMedia() + mediaResource.getChildren(folder.id).then(function (data) { + folder.children = data.items ? data.items : []; + angular.forEach(folder.children, function (child) { + child.isFolder = child.contentTypeAlias == 'Folder' ? true : false; + if (!child.isFolder) { + angular.forEach(child.properties, function (property) { + if (property.alias == 'umbracoFile' && property.value) { + child.thumbnail = mediaHelper.resolveFile(child, true); + child.image = property.value; + } + }); + } + }); + $scope.options.formData.currentFolder = folder.id; + $scope.currentFolder = folder; + }); + }; + $scope.iconFolder = 'glyphicons-icon folder-open'; + $scope.selectMedia = function (media) { + if (!media.isFolder) { + //we have 3 options add to collection (if multi) show details, or submit it right back to the callback + $scope.selectedMedia = media; + modalFieldvalue = 'url(' + $scope.selectedMedia.image + ')'; + $scope.change(modalFieldvalue); + } else { + $scope.gotoFolder(media); + } + }; + //default root item + if (!$scope.selectedMedia) { + $scope.gotoFolder(); + } + $scope.submitAndClose = function () { + if (modalFieldvalue != '') { + $scope.submit(modalFieldvalue); + } else { + $scope.cancel(); + } + }; + $scope.cancelAndClose = function () { + $scope.cancel(); + }; + }); + /*********************************************************************************************************/ + /* Background editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.border', function ($scope, dialogService) { + $scope.defaultBorderList = [ + 'all', + 'left', + 'right', + 'top', + 'bottom' + ]; + $scope.borderList = []; + $scope.bordertypes = [ + 'solid', + 'dashed', + 'dotted' + ]; + $scope.selectedBorder = { + name: 'all', + size: 0, + color: '', + type: '' + }; + $scope.setselectedBorder = function (bordertype) { + if (bordertype == 'all') { + $scope.selectedBorder.name = 'all'; + $scope.selectedBorder.size = $scope.item.values.bordersize; + $scope.selectedBorder.color = $scope.item.values.bordercolor; + $scope.selectedBorder.type = $scope.item.values.bordertype; + } + if (bordertype == 'left') { + $scope.selectedBorder.name = 'left'; + $scope.selectedBorder.size = $scope.item.values.leftbordersize; + $scope.selectedBorder.color = $scope.item.values.leftbordercolor; + $scope.selectedBorder.type = $scope.item.values.leftbordertype; + } + if (bordertype == 'right') { + $scope.selectedBorder.name = 'right'; + $scope.selectedBorder.size = $scope.item.values.rightbordersize; + $scope.selectedBorder.color = $scope.item.values.rightbordercolor; + $scope.selectedBorder.type = $scope.item.values.rightbordertype; + } + if (bordertype == 'top') { + $scope.selectedBorder.name = 'top'; + $scope.selectedBorder.size = $scope.item.values.topbordersize; + $scope.selectedBorder.color = $scope.item.values.topbordercolor; + $scope.selectedBorder.type = $scope.item.values.topbordertype; + } + if (bordertype == 'bottom') { + $scope.selectedBorder.name = 'bottom'; + $scope.selectedBorder.size = $scope.item.values.bottombordersize; + $scope.selectedBorder.color = $scope.item.values.bottombordercolor; + $scope.selectedBorder.type = $scope.item.values.bottombordertype; + } + }; + if (!$scope.item.values) { + $scope.item.values = { + bordersize: '', + bordercolor: '', + bordertype: 'solid', + leftbordersize: '', + leftbordercolor: '', + leftbordertype: 'solid', + rightbordersize: '', + rightbordercolor: '', + rightbordertype: 'solid', + topbordersize: '', + topbordercolor: '', + topbordertype: 'solid', + bottombordersize: '', + bottombordercolor: '', + bottombordertype: 'solid' + }; + } + if ($scope.item.enable) { + angular.forEach($scope.defaultBorderList, function (key, indexKey) { + if ($.inArray(key, $scope.item.enable) >= 0) { + $scope.borderList.splice($scope.borderList.length + 1, 0, key); + } + }); + } else { + $scope.borderList = $scope.defaultBorderList; + } + $scope.$watch('valueAreLoaded', function () { + $scope.setselectedBorder($scope.borderList[0]); + }, false); + $scope.$watch('selectedBorder', function () { + if ($scope.selectedBorder.name == 'all') { + $scope.item.values.bordersize = $scope.selectedBorder.size; + $scope.item.values.bordertype = $scope.selectedBorder.type; + } + if ($scope.selectedBorder.name == 'left') { + $scope.item.values.leftbordersize = $scope.selectedBorder.size; + $scope.item.values.leftbordertype = $scope.selectedBorder.type; + } + if ($scope.selectedBorder.name == 'right') { + $scope.item.values.rightbordersize = $scope.selectedBorder.size; + $scope.item.values.rightbordertype = $scope.selectedBorder.type; + } + if ($scope.selectedBorder.name == 'top') { + $scope.item.values.topbordersize = $scope.selectedBorder.size; + $scope.item.values.topbordertype = $scope.selectedBorder.type; + } + if ($scope.selectedBorder.name == 'bottom') { + $scope.item.values.bottombordersize = $scope.selectedBorder.size; + $scope.item.values.bottombordertype = $scope.selectedBorder.type; + } + }, true); + }); + /*********************************************************************************************************/ + /* color editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.color', function ($scope) { + if (!$scope.item.values) { + $scope.item.values = { color: '' }; + } + }); + /*********************************************************************************************************/ + /* google font editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.googlefontpicker', function ($scope, dialogService) { + if (!$scope.item.values) { + $scope.item.values = { + fontFamily: '', + fontType: '', + fontWeight: '', + fontStyle: '' + }; + } + $scope.setStyleVariant = function () { + if ($scope.item.values != undefined) { + return { + 'font-family': $scope.item.values.fontFamily, + 'font-weight': $scope.item.values.fontWeight, + 'font-style': $scope.item.values.fontStyle + }; + } + }; + $scope.open = function (field) { + var config = { + template: 'googlefontdialog.html', + change: function (data) { + $scope.item.values = data; + }, + callback: function (data) { + $scope.item.values = data; + }, + cancel: function (data) { + $scope.item.values = data; + }, + dialogData: $scope.googleFontFamilies, + dialogItem: $scope.item.values + }; + dialogService.open(config); + }; + }).controller('googlefontdialog.controller', function ($scope) { + $scope.safeFonts = [ + 'Arial, Helvetica', + 'Impact', + 'Lucida Sans Unicode', + 'Tahoma', + 'Trebuchet MS', + 'Verdana', + 'Georgia', + 'Times New Roman', + 'Courier New, Courier' + ]; + $scope.fonts = []; + $scope.selectedFont = {}; + var googleGetWeight = function (googleVariant) { + return googleVariant != undefined && googleVariant != '' ? googleVariant.replace('italic', '') : ''; + }; + var googleGetStyle = function (googleVariant) { + var variantStyle = ''; + if (googleVariant != undefined && googleVariant != '' && googleVariant.indexOf('italic') >= 0) { + variantWeight = googleVariant.replace('italic', ''); + variantStyle = 'italic'; + } + return variantStyle; + }; + angular.forEach($scope.safeFonts, function (value, key) { + $scope.fonts.push({ + groupName: 'Safe fonts', + fontType: 'safe', + fontFamily: value, + fontWeight: 'normal', + fontStyle: 'normal' + }); + }); + angular.forEach($scope.dialogData.items, function (value, key) { + var variants = value.variants; + var variant = value.variants.length > 0 ? value.variants[0] : ''; + var fontWeight = googleGetWeight(variant); + var fontStyle = googleGetStyle(variant); + $scope.fonts.push({ + groupName: 'Google fonts', + fontType: 'google', + fontFamily: value.family, + variants: value.variants, + variant: variant, + fontWeight: fontWeight, + fontStyle: fontStyle + }); + }); + $scope.setStyleVariant = function () { + if ($scope.dialogItem != undefined) { + return { + 'font-family': $scope.selectedFont.fontFamily, + 'font-weight': $scope.selectedFont.fontWeight, + 'font-style': $scope.selectedFont.fontStyle + }; + } + }; + function loadFont(font, variant) { + WebFont.load({ + google: { families: [font.fontFamily + ':' + variant] }, + loading: function () { + console.log('loading'); + }, + active: function () { + $scope.selectedFont = font; + $scope.selectedFont.fontWeight = googleGetWeight(variant); + $scope.selectedFont.fontStyle = googleGetStyle(variant); + // If $apply isn't called, the new font family isn't applied until the next user click. + $scope.change({ + fontFamily: $scope.selectedFont.fontFamily, + fontType: $scope.selectedFont.fontType, + fontWeight: $scope.selectedFont.fontWeight, + fontStyle: $scope.selectedFont.fontStyle + }); + } + }); + } + var webFontScriptLoaded = false; + $scope.showFontPreview = function (font, variant) { + if (!variant) + variant = font.variant; + if (font != undefined && font.fontFamily != '' && font.fontType == 'google') { + // Font needs to be independently loaded in the iframe for live preview to work. + document.getElementById('resultFrame').contentWindow.getFont(font.fontFamily + ':' + variant); + if (!webFontScriptLoaded) { + $.getScript('https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js').done(function () { + webFontScriptLoaded = true; + loadFont(font, variant); + }).fail(function () { + console.log('error loading webfont'); + }); + } else { + loadFont(font, variant); + } + } else { + // Font is available, apply it immediately in modal preview. + $scope.selectedFont = font; + // If $apply isn't called, the new font family isn't applied until the next user click. + $scope.change({ + fontFamily: $scope.selectedFont.fontFamily, + fontType: $scope.selectedFont.fontType, + fontWeight: $scope.selectedFont.fontWeight, + fontStyle: $scope.selectedFont.fontStyle + }); + } + }; + $scope.cancelAndClose = function () { + $scope.cancel(); + }; + $scope.submitAndClose = function () { + $scope.submit({ + fontFamily: $scope.selectedFont.fontFamily, + fontType: $scope.selectedFont.fontType, + fontWeight: $scope.selectedFont.fontWeight, + fontStyle: $scope.selectedFont.fontStyle + }); + }; + if ($scope.dialogItem != undefined) { + angular.forEach($scope.fonts, function (value, key) { + if (value.fontFamily == $scope.dialogItem.fontFamily) { + $scope.selectedFont = value; + $scope.selectedFont.variant = $scope.dialogItem.fontWeight + $scope.dialogItem.fontStyle; + $scope.selectedFont.fontWeight = $scope.dialogItem.fontWeight; + $scope.selectedFont.fontStyle = $scope.dialogItem.fontStyle; + } + }); + } + }); + /*********************************************************************************************************/ + /* grid row editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.gridRow', function ($scope) { + if (!$scope.item.values) { + $scope.item.values = { fullsize: false }; + } + }); + /*********************************************************************************************************/ + /* Layout */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.layout', function ($scope) { + if (!$scope.item.values) { + $scope.item.values = { layout: '' }; + } + }); + /*********************************************************************************************************/ + /* margin editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.margin', function ($scope, dialogService) { + $scope.defaultmarginList = [ + 'all', + 'left', + 'right', + 'top', + 'bottom' + ]; + $scope.marginList = []; + $scope.selectedmargin = { + name: '', + value: 0 + }; + $scope.setSelectedmargin = function (margintype) { + if (margintype == 'all') { + $scope.selectedmargin.name = 'all'; + $scope.selectedmargin.value = $scope.item.values.marginvalue; + } + if (margintype == 'left') { + $scope.selectedmargin.name = 'left'; + $scope.selectedmargin.value = $scope.item.values.leftmarginvalue; + } + if (margintype == 'right') { + $scope.selectedmargin.name = 'right'; + $scope.selectedmargin.value = $scope.item.values.rightmarginvalue; + } + if (margintype == 'top') { + $scope.selectedmargin.name = 'top'; + $scope.selectedmargin.value = $scope.item.values.topmarginvalue; + } + if (margintype == 'bottom') { + $scope.selectedmargin.name = 'bottom'; + $scope.selectedmargin.value = $scope.item.values.bottommarginvalue; + } + }; + if (!$scope.item.values) { + $scope.item.values = { + marginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 0 ? $scope.item.defaultValue[0] : '', + leftmarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 1 ? $scope.item.defaultValue[1] : '', + rightmarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 2 ? $scope.item.defaultValue[2] : '', + topmarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 3 ? $scope.item.defaultValue[3] : '', + bottommarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 4 ? $scope.item.defaultValue[4] : '' + }; + } + if ($scope.item.enable) { + angular.forEach($scope.defaultmarginList, function (key, indexKey) { + if ($.inArray(key, $scope.item.enable) >= 0) { + $scope.marginList.splice($scope.marginList.length + 1, 0, key); + } + }); + } else { + $scope.marginList = $scope.defaultmarginList; + } + $scope.$watch('valueAreLoaded', function () { + $scope.setSelectedmargin($scope.marginList[0]); + }, false); + $scope.$watch('selectedmargin', function () { + if ($scope.selectedmargin.name == 'all') { + $scope.item.values.marginvalue = $scope.selectedmargin.value; + } + if ($scope.selectedmargin.name == 'left') { + $scope.item.values.leftmarginvalue = $scope.selectedmargin.value; + } + if ($scope.selectedmargin.name == 'right') { + $scope.item.values.rightmarginvalue = $scope.selectedmargin.value; + } + if ($scope.selectedmargin.name == 'top') { + $scope.item.values.topmarginvalue = $scope.selectedmargin.value; + } + if ($scope.selectedmargin.name == 'bottom') { + $scope.item.values.bottommarginvalue = $scope.selectedmargin.value; + } + }, true); + }); + /*********************************************************************************************************/ + /* padding editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.padding', function ($scope, dialogService) { + $scope.defaultPaddingList = [ + 'all', + 'left', + 'right', + 'top', + 'bottom' + ]; + $scope.paddingList = []; + $scope.selectedpadding = { + name: '', + value: 0 + }; + $scope.setSelectedpadding = function (paddingtype) { + if (paddingtype == 'all') { + $scope.selectedpadding.name = 'all'; + $scope.selectedpadding.value = $scope.item.values.paddingvalue; + } + if (paddingtype == 'left') { + $scope.selectedpadding.name = 'left'; + $scope.selectedpadding.value = $scope.item.values.leftpaddingvalue; + } + if (paddingtype == 'right') { + $scope.selectedpadding.name = 'right'; + $scope.selectedpadding.value = $scope.item.values.rightpaddingvalue; + } + if (paddingtype == 'top') { + $scope.selectedpadding.name = 'top'; + $scope.selectedpadding.value = $scope.item.values.toppaddingvalue; + } + if (paddingtype == 'bottom') { + $scope.selectedpadding.name = 'bottom'; + $scope.selectedpadding.value = $scope.item.values.bottompaddingvalue; + } + }; + if (!$scope.item.values) { + $scope.item.values = { + paddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 0 ? $scope.item.defaultValue[0] : '', + leftpaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 1 ? $scope.item.defaultValue[1] : '', + rightpaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 2 ? $scope.item.defaultValue[2] : '', + toppaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 3 ? $scope.item.defaultValue[3] : '', + bottompaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 4 ? $scope.item.defaultValue[4] : '' + }; + } + if ($scope.item.enable) { + angular.forEach($scope.defaultPaddingList, function (key, indexKey) { + if ($.inArray(key, $scope.item.enable) >= 0) { + $scope.paddingList.splice($scope.paddingList.length + 1, 0, key); + } + }); + } else { + $scope.paddingList = $scope.defaultPaddingList; + } + $scope.$watch('valueAreLoaded', function () { + $scope.setSelectedpadding($scope.paddingList[0]); + }, false); + $scope.$watch('selectedpadding', function () { + if ($scope.selectedpadding.name == 'all') { + $scope.item.values.paddingvalue = $scope.selectedpadding.value; + } + if ($scope.selectedpadding.name == 'left') { + $scope.item.values.leftpaddingvalue = $scope.selectedpadding.value; + } + if ($scope.selectedpadding.name == 'right') { + $scope.item.values.rightpaddingvalue = $scope.selectedpadding.value; + } + if ($scope.selectedpadding.name == 'top') { + $scope.item.values.toppaddingvalue = $scope.selectedpadding.value; + } + if ($scope.selectedpadding.name == 'bottom') { + $scope.item.values.bottompaddingvalue = $scope.selectedpadding.value; + } + }, true); + }); + /*********************************************************************************************************/ + /* radius editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.radius', function ($scope, dialogService) { + $scope.defaultRadiusList = [ + 'all', + 'topleft', + 'topright', + 'bottomleft', + 'bottomright' + ]; + $scope.radiusList = []; + $scope.selectedradius = { + name: '', + value: 0 + }; + $scope.setSelectedradius = function (radiustype) { + if (radiustype == 'all') { + $scope.selectedradius.name = 'all'; + $scope.selectedradius.value = $scope.item.values.radiusvalue; + } + if (radiustype == 'topleft') { + $scope.selectedradius.name = 'topleft'; + $scope.selectedradius.value = $scope.item.values.topleftradiusvalue; + } + if (radiustype == 'topright') { + $scope.selectedradius.name = 'topright'; + $scope.selectedradius.value = $scope.item.values.toprightradiusvalue; + } + if (radiustype == 'bottomleft') { + $scope.selectedradius.name = 'bottomleft'; + $scope.selectedradius.value = $scope.item.values.bottomleftradiusvalue; + } + if (radiustype == 'bottomright') { + $scope.selectedradius.name = 'bottomright'; + $scope.selectedradius.value = $scope.item.values.bottomrightradiusvalue; + } + }; + if (!$scope.item.values) { + $scope.item.values = { + radiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 0 ? $scope.item.defaultValue[0] : '', + topleftradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 1 ? $scope.item.defaultValue[1] : '', + toprightradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 2 ? $scope.item.defaultValue[2] : '', + bottomleftradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 3 ? $scope.item.defaultValue[3] : '', + bottomrightradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 4 ? $scope.item.defaultValue[4] : '' + }; + } + if ($scope.item.enable) { + angular.forEach($scope.defaultRadiusList, function (key, indexKey) { + if ($.inArray(key, $scope.item.enable) >= 0) { + $scope.radiusList.splice($scope.radiusList.length + 1, 0, key); + } + }); + } else { + $scope.radiusList = $scope.defaultRadiusList; + } + $scope.$watch('valueAreLoaded', function () { + $scope.setSelectedradius($scope.radiusList[0]); + }, false); + $scope.$watch('selectedradius', function () { + if ($scope.selectedradius.name == 'all') { + $scope.item.values.radiusvalue = $scope.selectedradius.value; + } + if ($scope.selectedradius.name == 'topleft') { + $scope.item.values.topleftradiusvalue = $scope.selectedradius.value; + } + if ($scope.selectedradius.name == 'topright') { + $scope.item.values.toprightradiusvalue = $scope.selectedradius.value; + } + if ($scope.selectedradius.name == 'bottomleft') { + $scope.item.values.bottomleftradiusvalue = $scope.selectedradius.value; + } + if ($scope.selectedradius.name == 'bottomright') { + $scope.item.values.bottomrightradiusvalue = $scope.selectedradius.value; + } + }, true); + }); + /*********************************************************************************************************/ + /* shadow editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.shadow', function ($scope) { + if (!$scope.item.values) { + $scope.item.values = { shadow: '' }; + } + }); + /*********************************************************************************************************/ + /* slider editor */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').controller('Umbraco.canvasdesigner.slider', function ($scope) { + if (!$scope.item.values) { + $scope.item.values = { slider: '' }; + } + }); + /*********************************************************************************************************/ + /* spectrum color picker directive */ + /*********************************************************************************************************/ + angular.module('colorpicker', ['spectrumcolorpicker']).directive('colorpicker', [ + 'dialogService', + function (dialogService) { + return { + restrict: 'EA', + scope: { ngModel: '=' }, + link: function (scope, $element) { + scope.openColorDialog = function () { + var config = { + template: 'colorModal.html', + change: function (data) { + scope.ngModel = data; + }, + callback: function (data) { + scope.ngModel = data; + }, + cancel: function (data) { + scope.ngModel = data; + }, + dialogItem: scope.ngModel, + scope: scope + }; + dialogService.open(config); + }; + scope.setColor = false; + scope.submitAndClose = function () { + if (scope.ngModel != '') { + scope.setColor = true; + scope.submit(scope.ngModel); + } else { + scope.cancel(); + } + }; + scope.cancelAndClose = function () { + scope.cancel(); + }; + }, + template: '
' + '
' + '' + '
', + replace: true + }; + } + ]); + /*********************************************************************************************************/ + /* jQuery UI Slider plugin wrapper */ + /*********************************************************************************************************/ + angular.module('Umbraco.canvasdesigner').factory('dialogService', function ($rootScope, $q, $http, $timeout, $compile, $templateCache) { + function closeDialog(dialog, destroyScope) { + if (dialog.element) { + dialog.element.removeClass('selected'); + dialog.element.html(''); + if (destroyScope) { + dialog.scope.$destroy(); + } + } + } + function open() { + } + return { + open: function (options) { + var defaults = { + template: '', + callback: undefined, + change: undefined, + cancel: undefined, + element: undefined, + dialogItem: undefined, + dialogData: undefined + }; + var dialog = angular.extend(defaults, options); + var destroyScope = true; + if (options && options.scope) { + destroyScope = false; + } + var scope = options && options.scope || $rootScope.$new(); + // Save original value for cancel action + var originalDialogItem = angular.copy(dialog.dialogItem); + dialog.element = $('.float-panel'); + /************************************/ + // Close dialog if the user clicks outside the dialog. (Not working well with colorpickers and datepickers) + $(document).mousedown(function (e) { + var container = dialog.element; + if (!container.is(e.target) && container.has(e.target).length === 0) { + closeDialog(dialog, destroyScope); + } + }); + /************************************/ + $q.when($templateCache.get(dialog.template) || $http.get(dialog.template, { cache: true }).then(function (res) { + return res.data; + })).then(function onSuccess(template) { + dialog.element.html(template); + $timeout(function () { + $compile(dialog.element)(scope); + }); + dialog.element.addClass('selected'); + scope.cancel = function () { + if (dialog.cancel) { + dialog.cancel(originalDialogItem); + } + closeDialog(dialog, destroyScope); + }; + scope.change = function (data) { + if (dialog.change) { + dialog.change(data); + } + }; + scope.submit = function (data) { + if (dialog.callback) { + dialog.callback(data); + } + closeDialog(dialog, destroyScope); + }; + scope.close = function () { + closeDialog(dialog, destroyScope); + }; + scope.dialogData = dialog.dialogData; + scope.dialogItem = dialog.dialogItem; + dialog.scope = scope; + }); + return dialog; + }, + close: function () { + var modal = $('.float-panel'); + modal.removeClass('selected'); + } + }; + }); + /*********************************************************************************************************/ + /* jQuery UI Slider plugin wrapper */ + /*********************************************************************************************************/ + angular.module('ui.slider', []).value('uiSliderConfig', {}).directive('uiSlider', [ + 'uiSliderConfig', + '$timeout', + function (uiSliderConfig, $timeout) { + uiSliderConfig = uiSliderConfig || {}; + return { + require: 'ngModel', + template: '
', + replace: true, + compile: function () { + return function (scope, elm, attrs, ngModel) { + scope.value = ngModel.$viewValue; + function parseNumber(n, decimals) { + return decimals ? parseFloat(n) : parseInt(n); + } + ; + var options = angular.extend(scope.$eval(attrs.uiSlider) || {}, uiSliderConfig); + // Object holding range values + var prevRangeValues = { + min: null, + max: null + }; + // convenience properties + var properties = [ + 'min', + 'max', + 'step' + ]; + var useDecimals = !angular.isUndefined(attrs.useDecimals) ? true : false; + var init = function () { + // When ngModel is assigned an array of values then range is expected to be true. + // Warn user and change range to true else an error occurs when trying to drag handle + if (angular.isArray(ngModel.$viewValue) && options.range !== true) { + console.warn('Change your range option of ui-slider. When assigning ngModel an array of values then the range option should be set to true.'); + options.range = true; + } + // Ensure the convenience properties are passed as options if they're defined + // This avoids init ordering issues where the slider's initial state (eg handle + // position) is calculated using widget defaults + // Note the properties take precedence over any duplicates in options + angular.forEach(properties, function (property) { + if (angular.isDefined(attrs[property])) { + options[property] = parseNumber(attrs[property], useDecimals); + } + }); + elm.find('.slider').slider(options); + init = angular.noop; + }; + // Find out if decimals are to be used for slider + angular.forEach(properties, function (property) { + // support {{}} and watch for updates + attrs.$observe(property, function (newVal) { + if (!!newVal) { + init(); + elm.find('.slider').slider('option', property, parseNumber(newVal, useDecimals)); + } + }); + }); + attrs.$observe('disabled', function (newVal) { + init(); + elm.find('.slider').slider('option', 'disabled', !!newVal); + }); + // Watch ui-slider (byVal) for changes and update + scope.$watch(attrs.uiSlider, function (newVal) { + init(); + if (newVal != undefined) { + elm.find('.slider').slider('option', newVal); + elm.find('.ui-slider-handle').html('' + ui.value + 'px'); + } + }, true); + // Late-bind to prevent compiler clobbering + $timeout(init, 0, true); + // Update model value from slider + elm.find('.slider').bind('slidestop', function (event, ui) { + ngModel.$setViewValue(ui.values || ui.value); + scope.$apply(); + }); + elm.bind('slide', function (event, ui) { + event.stopPropagation(); + elm.find('.slider-input').val(ui.value); + elm.find('.ui-slider-handle').html('' + ui.value + 'px'); + }); + // Update slider from model value + ngModel.$render = function () { + init(); + var method = options.range === true ? 'values' : 'value'; + if (isNaN(ngModel.$viewValue) && !(ngModel.$viewValue instanceof Array)) + ngModel.$viewValue = 0; + if (ngModel.$viewValue == '') + ngModel.$viewValue = 0; + scope.value = ngModel.$viewValue; + // Do some sanity check of range values + if (options.range === true) { + // Check outer bounds for min and max values + if (angular.isDefined(options.min) && options.min > ngModel.$viewValue[0]) { + ngModel.$viewValue[0] = options.min; + } + if (angular.isDefined(options.max) && options.max < ngModel.$viewValue[1]) { + ngModel.$viewValue[1] = options.max; + } + // Check min and max range values + if (ngModel.$viewValue[0] >= ngModel.$viewValue[1]) { + // Min value should be less to equal to max value + if (prevRangeValues.min >= ngModel.$viewValue[1]) + ngModel.$viewValue[0] = prevRangeValues.min; + // Max value should be less to equal to min value + if (prevRangeValues.max <= ngModel.$viewValue[0]) + ngModel.$viewValue[1] = prevRangeValues.max; + } + // Store values for later user + prevRangeValues.min = ngModel.$viewValue[0]; + prevRangeValues.max = ngModel.$viewValue[1]; + } + elm.find('.slider').slider(method, ngModel.$viewValue); + elm.find('.ui-slider-handle').html('' + ngModel.$viewValue + 'px'); + }; + scope.$watch('value', function () { + ngModel.$setViewValue(scope.value); + }, true); + scope.$watch(attrs.ngModel, function () { + if (options.range === true) { + ngModel.$render(); + } + }, true); + function destroy() { + elm.find('.slider').slider('destroy'); + } + elm.find('.slider').bind('$destroy', destroy); + }; + } + }; + } + ]); + /*********************************************************************************************************/ + /* spectrum color picker directive */ + /*********************************************************************************************************/ + angular.module('spectrumcolorpicker', []).directive('spectrum', function () { + return { + restrict: 'E', + transclude: true, + scope: { + colorselected: '=', + setColor: '=', + flat: '=', + showPalette: '=' + }, + link: function (scope, $element) { + var initColor; + $element.find('input').spectrum({ + color: scope.colorselected, + allowEmpty: true, + preferredFormat: 'hex', + showAlpha: true, + showInput: true, + flat: scope.flat, + localStorageKey: 'spectrum.panel', + showPalette: scope.showPalette, + palette: [], + change: function (color) { + if (color) { + scope.colorselected = color.toRgbString(); + } else { + scope.colorselected = ''; + } + scope.$apply(); + }, + move: function (color) { + scope.colorselected = color.toRgbString(); + scope.$apply(); + }, + beforeShow: function (color) { + initColor = angular.copy(scope.colorselected); + $(this).spectrum('container').find('.sp-cancel').click(function (e) { + scope.colorselected = initColor; + scope.$apply(); + }); + } + }); + scope.$watch('setcolor', function (setColor) { + if (scope.$eval(setColor) === true) { + $element.find('input').spectrum('set', scope.colorselected); + } + }, true); + }, + template: '
', + replace: true + }; + }); +}()); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.controllers.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.controllers.js index f78e7654..5fdb113e 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.controllers.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.controllers.js @@ -1,489 +1,416 @@ -/*! umbraco - * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2017 Umbraco HQ; - * Licensed - */ - -(function() { - - -/** - * @ngdoc controller - * @name Umbraco.MainController - * @function - * - * @description - * The main application controller - * - */ -function MainController($scope, $rootScope, $location, $routeParams, $timeout, $http, $log, appState, treeService, notificationsService, userService, navigationService, historyService, updateChecker, assetsService, eventsService, umbRequestHelper, tmhDynamicLocale, localStorageService) { - - //the null is important because we do an explicit bool check on this in the view - //the avatar is by default the umbraco logo - $scope.authenticated = null; - $scope.avatar = [ - { value: "assets/img/application/logo.png" }, - { value: "assets/img/application/logo@2x.png" }, - { value: "assets/img/application/logo@3x.png" } - ]; - $scope.touchDevice = appState.getGlobalState("touchDevice"); - - - $scope.removeNotification = function (index) { - notificationsService.remove(index); - }; - - $scope.closeDialogs = function (event) { - //only close dialogs if non-link and non-buttons are clicked - var el = event.target.nodeName; - var els = ["INPUT", "A", "BUTTON"]; - - if (els.indexOf(el) >= 0) { return; } - - var parents = $(event.target).parents("a,button"); - if (parents.length > 0) { - return; - } - - //SD: I've updated this so that we don't close the dialog when clicking inside of the dialog - var nav = $(event.target).parents("#dialog"); - if (nav.length === 1) { - return; - } - - eventsService.emit("app.closeDialogs", event); - }; - - var evts = []; - - //when a user logs out or timesout - evts.push(eventsService.on("app.notAuthenticated", function () { +(function () { + function MainController($scope, $rootScope, $location, $routeParams, $timeout, $http, $log, appState, treeService, notificationsService, userService, navigationService, historyService, updateChecker, assetsService, eventsService, umbRequestHelper, tmhDynamicLocale, localStorageService) { + //the null is important because we do an explicit bool check on this in the view + //the avatar is by default the umbraco logo $scope.authenticated = null; - $scope.user = null; - })); - - //when the app is read/user is logged in, setup the data - evts.push(eventsService.on("app.ready", function (evt, data) { - - $scope.authenticated = data.authenticated; - $scope.user = data.user; - - updateChecker.check().then(function(update) { - if (update && update !== "null") { - if (update.type !== "None") { - var notification = { - headline: "Update available", - message: "Click to download", - sticky: true, - type: "info", - url: update.url - }; - notificationsService.add(notification); + $scope.avatar = [ + { value: 'assets/img/application/logo.png' }, + { value: 'assets/img/application/logo@2x.png' }, + { value: 'assets/img/application/logo@3x.png' } + ]; + $scope.touchDevice = appState.getGlobalState('touchDevice'); + $scope.removeNotification = function (index) { + notificationsService.remove(index); + }; + $scope.closeDialogs = function (event) { + //only close dialogs if non-link and non-buttons are clicked + var el = event.target.nodeName; + var els = [ + 'INPUT', + 'A', + 'BUTTON' + ]; + if (els.indexOf(el) >= 0) { + return; + } + var parents = $(event.target).parents('a,button'); + if (parents.length > 0) { + return; + } + //SD: I've updated this so that we don't close the dialog when clicking inside of the dialog + var nav = $(event.target).parents('#dialog'); + if (nav.length === 1) { + return; + } + eventsService.emit('app.closeDialogs', event); + }; + var evts = []; + //when a user logs out or timesout + evts.push(eventsService.on('app.notAuthenticated', function () { + $scope.authenticated = null; + $scope.user = null; + })); + evts.push(eventsService.on('app.userRefresh', function (evt) { + userService.refreshCurrentUser().then(function (data) { + $scope.user = data; + //Load locale file + if ($scope.user.locale) { + tmhDynamicLocale.set($scope.user.locale); + } + if ($scope.user.avatars) { + $scope.avatar = []; + if (angular.isArray($scope.user.avatars)) { + for (var i = 0; i < $scope.user.avatars.length; i++) { + $scope.avatar.push({ value: $scope.user.avatars[i] }); + } + } + } + }); + })); + //when the app is ready/user is logged in, setup the data + evts.push(eventsService.on('app.ready', function (evt, data) { + $scope.authenticated = data.authenticated; + $scope.user = data.user; + updateChecker.check().then(function (update) { + if (update && update !== 'null') { + if (update.type !== 'None') { + var notification = { + headline: 'Update available', + message: 'Click to download', + sticky: true, + type: 'info', + url: update.url + }; + notificationsService.add(notification); + } + } + }); + //if the user has changed we need to redirect to the root so they don't try to continue editing the + //last item in the URL (NOTE: the user id can equal zero, so we cannot just do !data.lastUserId since that will resolve to true) + if (data.lastUserId !== undefined && data.lastUserId !== null && data.lastUserId !== data.user.id) { + $location.path('/').search(''); + historyService.removeAll(); + treeService.clearCache(); + //if the user changed, clearout local storage too - could contain sensitive data + localStorageService.clearAll(); + } + //if this is a new login (i.e. the user entered credentials), then clear out local storage - could contain sensitive data + if (data.loginType === 'credentials') { + localStorageService.clearAll(); + } + //Load locale file + if ($scope.user.locale) { + tmhDynamicLocale.set($scope.user.locale); + } + if ($scope.user.avatars) { + $scope.avatar = []; + if (angular.isArray($scope.user.avatars)) { + for (var i = 0; i < $scope.user.avatars.length; i++) { + $scope.avatar.push({ value: $scope.user.avatars[i] }); + } } } + })); + evts.push(eventsService.on('app.ysod', function (name, error) { + $scope.ysodOverlay = { + view: 'ysod', + error: error, + show: true + }; + })); + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } }); - - //if the user has changed we need to redirect to the root so they don't try to continue editing the - //last item in the URL (NOTE: the user id can equal zero, so we cannot just do !data.lastUserId since that will resolve to true) - if (data.lastUserId !== undefined && data.lastUserId !== null && data.lastUserId !== data.user.id) { - $location.path("/").search(""); - historyService.removeAll(); - treeService.clearCache(); - - //if the user changed, clearout local storage too - could contain sensitive data - localStorageService.clearAll(); - } - - //if this is a new login (i.e. the user entered credentials), then clear out local storage - could contain sensitive data - if (data.loginType === "credentials") { - localStorageService.clearAll(); - } - - //Load locale file - if ($scope.user.locale) { - tmhDynamicLocale.set($scope.user.locale); - } - - if ($scope.user.emailHash) { - - //let's attempt to load the avatar, it might not exist or we might not have - // internet access, well get an empty string back - $http.get(umbRequestHelper.getApiUrl("gravatarApiBaseUrl", "GetCurrentUserGravatarUrl")) - .then( - function successCallback(response) { - // if we can't download the gravatar for some reason, an null gets returned, we cannot do anything - if (response.data !== "null") { - if ($scope.user && $scope.user.emailHash) { - var avatarBaseUrl = "https://www.gravatar.com/avatar/"; - var hash = $scope.user.emailHash; - - $scope.avatar = [ - { value: avatarBaseUrl + hash + ".jpg?s=30&d=mm" }, - { value: avatarBaseUrl + hash + ".jpg?s=60&d=mm" }, - { value: avatarBaseUrl + hash + ".jpg?s=90&d=mm" } - ]; - } - } - - }, function errorCallback(response) { - //cannot load it from the server so we cannot do anything - }); - } - })); - - evts.push(eventsService.on("app.ysod", function (name, error) { - $scope.ysodOverlay = { - view: "ysod", - error: error, - show: true - }; - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } + } + //register it + angular.module('umbraco').controller('Umbraco.MainController', MainController).config(function (tmhDynamicLocaleProvider) { + //Set url for locale files + tmhDynamicLocaleProvider.localeLocationPattern('lib/angular/1.1.5/i18n/angular-locale_{{locale}}.js'); }); - -} - - -//register it -angular.module('umbraco').controller("Umbraco.MainController", MainController). - config(function (tmhDynamicLocaleProvider) { - //Set url for locale files - tmhDynamicLocaleProvider.localeLocationPattern('lib/angular/1.1.5/i18n/angular-locale_{{locale}}.js'); + /** + * @ngdoc controller + * @name Umbraco.NavigationController + * @function + * + * @description + * Handles the section area of the app + * + * @param {navigationService} navigationService A reference to the navigationService + */ + function NavigationController($scope, $rootScope, $location, $log, $routeParams, $timeout, appState, navigationService, keyboardService, dialogService, historyService, eventsService, sectionResource, angularHelper) { + //TODO: Need to think about this and an nicer way to acheive what this is doing. + //the tree event handler i used to subscribe to the main tree click events + $scope.treeEventHandler = $({}); + navigationService.setupTreeEvents($scope.treeEventHandler); + //Put the navigation service on this scope so we can use it's methods/properties in the view. + // IMPORTANT: all properties assigned to this scope are generally available on the scope object on dialogs since + // when we create a dialog we pass in this scope to be used for the dialog's scope instead of creating a new one. + $scope.nav = navigationService; + // TODO: Lets fix this, it is less than ideal to be passing in the navigationController scope to something else to be used as it's scope, + // this is going to lead to problems/confusion. I really don't think passing scope's around is very good practice. + $rootScope.nav = navigationService; + //set up our scope vars + $scope.showContextMenuDialog = false; + $scope.showContextMenu = false; + $scope.showSearchResults = false; + $scope.menuDialogTitle = null; + $scope.menuActions = []; + $scope.menuNode = null; + $scope.currentSection = appState.getSectionState('currentSection'); + $scope.showNavigation = appState.getGlobalState('showNavigation'); + //trigger search with a hotkey: + keyboardService.bind('ctrl+shift+s', function () { + navigationService.showSearch(); + }); + //trigger dialods with a hotkey: + keyboardService.bind('esc', function () { + eventsService.emit('app.closeDialogs'); + }); + $scope.selectedId = navigationService.currentId; + var evts = []; + //Listen for global state changes + evts.push(eventsService.on('appState.globalState.changed', function (e, args) { + if (args.key === 'showNavigation') { + $scope.showNavigation = args.value; + } + })); + //Listen for menu state changes + evts.push(eventsService.on('appState.menuState.changed', function (e, args) { + if (args.key === 'showMenuDialog') { + $scope.showContextMenuDialog = args.value; + } + if (args.key === 'showMenu') { + $scope.showContextMenu = args.value; + } + if (args.key === 'dialogTitle') { + $scope.menuDialogTitle = args.value; + } + if (args.key === 'menuActions') { + $scope.menuActions = args.value; + } + if (args.key === 'currentNode') { + $scope.menuNode = args.value; + } + })); + //Listen for section state changes + evts.push(eventsService.on('appState.treeState.changed', function (e, args) { + var f = args; + if (args.value.root && args.value.root.metaData.containsTrees === false) { + $rootScope.emptySection = true; + } else { + $rootScope.emptySection = false; + } + })); + //Listen for section state changes + evts.push(eventsService.on('appState.sectionState.changed', function (e, args) { + //section changed + if (args.key === 'currentSection') { + $scope.currentSection = args.value; + } + //show/hide search results + if (args.key === 'showSearchResults') { + $scope.showSearchResults = args.value; + } + })); + //This reacts to clicks passed to the body element which emits a global call to close all dialogs + evts.push(eventsService.on('app.closeDialogs', function (event) { + if (appState.getGlobalState('stickyNavigation')) { + navigationService.hideNavigation(); + //TODO: don't know why we need this? - we are inside of an angular event listener. + angularHelper.safeApply($scope); + } + })); + //when a user logs out or timesout + evts.push(eventsService.on('app.notAuthenticated', function () { + $scope.authenticated = false; + })); + //when the application is ready and the user is authorized setup the data + evts.push(eventsService.on('app.ready', function (evt, data) { + $scope.authenticated = true; + })); + //this reacts to the options item in the tree + //todo, migrate to nav service + $scope.searchShowMenu = function (ev, args) { + //always skip default + args.skipDefault = true; + navigationService.showMenu(ev, args); + }; + //todo, migrate to nav service + $scope.searchHide = function () { + navigationService.hideSearch(); + }; + //the below assists with hiding/showing the tree + var treeActive = false; + //Sets a service variable as soon as the user hovers the navigation with the mouse + //used by the leaveTree method to delay hiding + $scope.enterTree = function (event) { + treeActive = true; + }; + // Hides navigation tree, with a short delay, is cancelled if the user moves the mouse over the tree again + $scope.leaveTree = function (event) { + //this is a hack to handle IE touch events which freaks out due to no mouse events so the tree instantly shuts down + if (!event) { + return; + } + if (!appState.getGlobalState('touchDevice')) { + treeActive = false; + $timeout(function () { + if (!treeActive) { + navigationService.hideTree(); + } + }, 300); + } + }; + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } }); - - -/** - * @ngdoc controller - * @name Umbraco.NavigationController - * @function - * - * @description - * Handles the section area of the app - * - * @param {navigationService} navigationService A reference to the navigationService + } + //register it + angular.module('umbraco').controller('Umbraco.NavigationController', NavigationController); + /** + * @ngdoc controller + * @name Umbraco.SearchController + * @function + * + * @description + * Controls the search functionality in the site + * */ -function NavigationController($scope, $rootScope, $location, $log, $routeParams, $timeout, appState, navigationService, keyboardService, dialogService, historyService, eventsService, sectionResource, angularHelper) { - - //TODO: Need to think about this and an nicer way to acheive what this is doing. - //the tree event handler i used to subscribe to the main tree click events - $scope.treeEventHandler = $({}); - navigationService.setupTreeEvents($scope.treeEventHandler); - - //Put the navigation service on this scope so we can use it's methods/properties in the view. - // IMPORTANT: all properties assigned to this scope are generally available on the scope object on dialogs since - // when we create a dialog we pass in this scope to be used for the dialog's scope instead of creating a new one. - $scope.nav = navigationService; - // TODO: Lets fix this, it is less than ideal to be passing in the navigationController scope to something else to be used as it's scope, - // this is going to lead to problems/confusion. I really don't think passing scope's around is very good practice. - $rootScope.nav = navigationService; - - //set up our scope vars - $scope.showContextMenuDialog = false; - $scope.showContextMenu = false; - $scope.showSearchResults = false; - $scope.menuDialogTitle = null; - $scope.menuActions = []; - $scope.menuNode = null; - - $scope.currentSection = appState.getSectionState("currentSection"); - $scope.showNavigation = appState.getGlobalState("showNavigation"); - - //trigger search with a hotkey: - keyboardService.bind("ctrl+shift+s", function () { - navigationService.showSearch(); - }); - - //trigger dialods with a hotkey: - keyboardService.bind("esc", function () { - eventsService.emit("app.closeDialogs"); - }); - - $scope.selectedId = navigationService.currentId; - - var evts = []; - - //Listen for global state changes - evts.push(eventsService.on("appState.globalState.changed", function(e, args) { - if (args.key === "showNavigation") { - $scope.showNavigation = args.value; - } - })); - - //Listen for menu state changes - evts.push(eventsService.on("appState.menuState.changed", function(e, args) { - if (args.key === "showMenuDialog") { - $scope.showContextMenuDialog = args.value; - } - if (args.key === "showMenu") { - $scope.showContextMenu = args.value; - } - if (args.key === "dialogTitle") { - $scope.menuDialogTitle = args.value; - } - if (args.key === "menuActions") { - $scope.menuActions = args.value; - } - if (args.key === "currentNode") { - $scope.menuNode = args.value; - } - })); - - //Listen for section state changes - evts.push(eventsService.on("appState.treeState.changed", function(e, args) { - var f = args; - if (args.value.root && args.value.root.metaData.containsTrees === false) { - $rootScope.emptySection = true; - } - else { - $rootScope.emptySection = false; - } - })); - - //Listen for section state changes - evts.push(eventsService.on("appState.sectionState.changed", function(e, args) { - //section changed - if (args.key === "currentSection") { - $scope.currentSection = args.value; - } - //show/hide search results - if (args.key === "showSearchResults") { - $scope.showSearchResults = args.value; - } - })); - - //This reacts to clicks passed to the body element which emits a global call to close all dialogs - evts.push(eventsService.on("app.closeDialogs", function(event) { - if (appState.getGlobalState("stickyNavigation")) { - navigationService.hideNavigation(); - //TODO: don't know why we need this? - we are inside of an angular event listener. - angularHelper.safeApply($scope); - } - })); - - //when a user logs out or timesout - evts.push(eventsService.on("app.notAuthenticated", function() { - $scope.authenticated = false; - })); - - //when the application is ready and the user is authorized setup the data - evts.push(eventsService.on("app.ready", function(evt, data) { - $scope.authenticated = true; - })); - - //this reacts to the options item in the tree - //todo, migrate to nav service - $scope.searchShowMenu = function (ev, args) { - //always skip default - args.skipDefault = true; - navigationService.showMenu(ev, args); - }; - - //todo, migrate to nav service - $scope.searchHide = function () { - navigationService.hideSearch(); - }; - - //the below assists with hiding/showing the tree - var treeActive = false; - - //Sets a service variable as soon as the user hovers the navigation with the mouse - //used by the leaveTree method to delay hiding - $scope.enterTree = function (event) { - treeActive = true; - }; - - // Hides navigation tree, with a short delay, is cancelled if the user moves the mouse over the tree again - $scope.leaveTree = function(event) { - //this is a hack to handle IE touch events which freaks out due to no mouse events so the tree instantly shuts down - if (!event) { - return; - } - if (!appState.getGlobalState("touchDevice")) { - treeActive = false; - $timeout(function() { - if (!treeActive) { - navigationService.hideTree(); + function SearchController($scope, searchService, $log, $location, navigationService, $q) { + $scope.searchTerm = null; + $scope.searchResults = []; + $scope.isSearching = false; + $scope.selectedResult = -1; + $scope.navigateResults = function (ev) { + //38: up 40: down, 13: enter + switch (ev.keyCode) { + case 38: + iterateResults(true); + break; + case 40: + iterateResults(false); + break; + case 13: + if ($scope.selectedItem) { + $location.path($scope.selectedItem.editorPath); + navigationService.hideSearch(); + } + break; + } + }; + var group = undefined; + var groupIndex = -1; + var itemIndex = -1; + $scope.selectedItem = undefined; + function iterateResults(up) { + //default group + if (!group) { + group = $scope.groups[0]; + groupIndex = 0; + } + if (up) { + if (itemIndex === 0) { + if (groupIndex === 0) { + gotoGroup($scope.groups.length - 1, true); + } else { + gotoGroup(groupIndex - 1, true); + } + } else { + gotoItem(itemIndex - 1); + } + } else { + if (itemIndex < group.results.length - 1) { + gotoItem(itemIndex + 1); + } else { + if (groupIndex === $scope.groups.length - 1) { + gotoGroup(0); + } else { + gotoGroup(groupIndex + 1); + } } - }, 300); + } + } + function gotoGroup(index, up) { + groupIndex = index; + group = $scope.groups[groupIndex]; + if (up) { + gotoItem(group.results.length - 1); + } else { + gotoItem(0); + } } - }; - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); + function gotoItem(index) { + itemIndex = index; + $scope.selectedItem = group.results[itemIndex]; } - }); -} - -//register it -angular.module('umbraco').controller("Umbraco.NavigationController", NavigationController); - -/** + //used to cancel any request in progress if another one needs to take it's place + var canceler = null; + $scope.$watch('searchTerm', _.debounce(function (newVal, oldVal) { + $scope.$apply(function () { + $scope.hasResults = false; + if ($scope.searchTerm) { + if (newVal !== null && newVal !== undefined && newVal !== oldVal) { + $scope.isSearching = true; + navigationService.showSearch(); + $scope.selectedItem = undefined; + //a canceler exists, so perform the cancelation operation and reset + if (canceler) { + canceler.resolve(); + canceler = $q.defer(); + } else { + canceler = $q.defer(); + } + searchService.searchAll({ + term: $scope.searchTerm, + canceler: canceler + }).then(function (result) { + //result is a dictionary of group Title and it's results + var filtered = {}; + _.each(result, function (value, key) { + if (value.results.length > 0) { + filtered[key] = value; + } + }); + $scope.groups = filtered; + // check if search has results + $scope.hasResults = Object.keys($scope.groups).length > 0; + //set back to null so it can be re-created + canceler = null; + $scope.isSearching = false; + }); + } + } else { + $scope.isSearching = false; + navigationService.hideSearch(); + $scope.selectedItem = undefined; + } + }); + }, 200)); + } + //register it + angular.module('umbraco').controller('Umbraco.SearchController', SearchController); + /** * @ngdoc controller - * @name Umbraco.SearchController + * @name Umbraco.MainController * @function * * @description - * Controls the search functionality in the site - * - */ -function SearchController($scope, searchService, $log, $location, navigationService, $q) { - - $scope.searchTerm = null; - $scope.searchResults = []; - $scope.isSearching = false; - $scope.selectedResult = -1; - - - $scope.navigateResults = function(ev){ - //38: up 40: down, 13: enter - - switch(ev.keyCode){ - case 38: - iterateResults(true); - break; - case 40: - iterateResults(false); - break; - case 13: - if ($scope.selectedItem) { - $location.path($scope.selectedItem.editorPath); - navigationService.hideSearch(); - } - break; - } - }; - - - var group = undefined; - var groupIndex = -1; - var itemIndex = -1; - $scope.selectedItem = undefined; - - - function iterateResults(up){ - //default group - if(!group){ - group = $scope.groups[0]; - groupIndex = 0; - } - - if(up){ - if(itemIndex === 0){ - if(groupIndex === 0){ - gotoGroup($scope.groups.length-1, true); - }else{ - gotoGroup(groupIndex-1, true); - } - }else{ - gotoItem(itemIndex-1); - } - }else{ - if(itemIndex < group.results.length-1){ - gotoItem(itemIndex+1); - }else{ - if(groupIndex === $scope.groups.length-1){ - gotoGroup(0); - }else{ - gotoGroup(groupIndex+1); - } - } - } - } - - function gotoGroup(index, up){ - groupIndex = index; - group = $scope.groups[groupIndex]; - - if(up){ - gotoItem(group.results.length-1); - }else{ - gotoItem(0); - } - } - - function gotoItem(index){ - itemIndex = index; - $scope.selectedItem = group.results[itemIndex]; - } - - //used to cancel any request in progress if another one needs to take it's place - var canceler = null; - - $scope.$watch("searchTerm", _.debounce(function (newVal, oldVal) { - $scope.$apply(function() { - if ($scope.searchTerm) { - if (newVal !== null && newVal !== undefined && newVal !== oldVal) { - $scope.isSearching = true; - navigationService.showSearch(); - $scope.selectedItem = undefined; - - //a canceler exists, so perform the cancelation operation and reset - if (canceler) { - canceler.resolve(); - canceler = $q.defer(); - } - else { - canceler = $q.defer(); - } - - searchService.searchAll({ term: $scope.searchTerm, canceler: canceler }).then(function(result) { - $scope.groups = _.filter(result, function (group) { return group.results.length > 0; }); - //set back to null so it can be re-created - canceler = null; - }); - } - } - else { - $scope.isSearching = false; - navigationService.hideSearch(); - $scope.selectedItem = undefined; - } - }); - }, 200)); - -} -//register it -angular.module('umbraco').controller("Umbraco.SearchController", SearchController); - -/** - * @ngdoc controller - * @name Umbraco.MainController - * @function - * - * @description - * The controller for the AuthorizeUpgrade login page - * + * The controller for the AuthorizeUpgrade login page + * */ -function AuthorizeUpgradeController($scope, $window) { - - //Add this method to the scope - this method will be called by the login dialog controller when the login is successful - // then we'll handle the redirect. - $scope.submit = function (event) { - - var qry = $window.location.search.trimStart("?").split("&"); - var redir = _.find(qry, function(item) { - return item.startsWith("redir="); - }); - if (redir) { - $window.location = decodeURIComponent(redir.split("=")[1]); - } - else { - $window.location = "/"; - } - - }; - -} - -angular.module('umbraco').controller("Umbraco.AuthorizeUpgradeController", AuthorizeUpgradeController); -/** + function AuthorizeUpgradeController($scope, $window) { + //Add this method to the scope - this method will be called by the login dialog controller when the login is successful + // then we'll handle the redirect. + $scope.submit = function (event) { + var qry = $window.location.search.trimStart('?').split('&'); + var redir = _.find(qry, function (item) { + return item.startsWith('redir='); + }); + if (redir) { + $window.location = decodeURIComponent(redir.split('=')[1]); + } else { + $window.location = '/'; + } + }; + } + angular.module('umbraco').controller('Umbraco.AuthorizeUpgradeController', AuthorizeUpgradeController); + /** * @ngdoc controller * @name Umbraco.DashboardController * @function @@ -491,118 +418,92 @@ angular.module('umbraco').controller("Umbraco.AuthorizeUpgradeController", Autho * @description * Controls the dashboards of the application * - */ - -function DashboardController($scope, $routeParams, dashboardResource, localizationService) { - - $scope.page = {}; - $scope.page.nameLocked = true; - $scope.page.loading = true; - - $scope.dashboard = {}; - localizationService.localize("sections_" + $routeParams.section).then(function(name){ - $scope.dashboard.name = name; - }); - - dashboardResource.getDashboard($routeParams.section).then(function(tabs){ - $scope.dashboard.tabs = tabs; - $scope.page.loading = false; - }); -} - - -//register it -angular.module('umbraco').controller("Umbraco.DashboardController", DashboardController); - -angular.module("umbraco") - .controller("Umbraco.Dialogs.ApprovedColorPickerController", function ($scope, $http, umbPropEditorHelper, assetsService) { - assetsService.loadJs("lib/cssparser/cssparser.js") - .then(function () { - - var cssPath = $scope.dialogData.cssPath; - $scope.cssClass = $scope.dialogData.cssClass; - - $scope.classes = []; - - $scope.change = function (newClass) { - $scope.model.value = newClass; - } - - $http.get(cssPath) - .success(function (data) { - var parser = new CSSParser(); - $scope.classes = parser.parse(data, false, false).cssRules; - $scope.classes.splice(0, 0, "noclass"); - }) - - assetsService.loadCss("/App_Plugins/Lecoati.uSky.Grid/lib/uSky.Grid.ApprovedColorPicker.css"); - assetsService.loadCss(cssPath); - }); -}); -function ContentEditDialogController($scope, editorState, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, umbModelMapper, $http) { - - $scope.defaultButton = null; - $scope.subButtons = []; - var dialogOptions = $scope.$parent.dialogOptions; - - // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish - function performSave(args) { - contentEditingHelper.contentEditorPerformSave({ - statusMessage: args.statusMessage, - saveMethod: args.saveMethod, - scope: $scope, - content: $scope.content - }).then(function (content) { - //success - if (dialogOptions.closeOnSave) { - $scope.submit(content); - } - - }, function(err) { - //error + */ + function DashboardController($scope, $routeParams, dashboardResource, localizationService) { + $scope.page = {}; + $scope.page.nameLocked = true; + $scope.page.loading = true; + $scope.dashboard = {}; + localizationService.localize('sections_' + $routeParams.section).then(function (name) { + $scope.dashboard.name = name; + }); + dashboardResource.getDashboard($routeParams.section).then(function (tabs) { + $scope.dashboard.tabs = tabs; + $scope.page.loading = false; }); } - - function filterTabs(entity, blackList) { - if (blackList) { - _.each(entity.tabs, function (tab) { - tab.hide = _.contains(blackList, tab.alias); + //register it + angular.module('umbraco').controller('Umbraco.DashboardController', DashboardController); + angular.module('umbraco').controller('Umbraco.Dialogs.ApprovedColorPickerController', function ($scope, $http, umbPropEditorHelper, assetsService) { + assetsService.loadJs('lib/cssparser/cssparser.js').then(function () { + var cssPath = $scope.dialogData.cssPath; + $scope.cssClass = $scope.dialogData.cssClass; + $scope.classes = []; + $scope.change = function (newClass) { + $scope.model.value = newClass; + }; + $http.get(cssPath).success(function (data) { + var parser = new CSSParser(); + $scope.classes = parser.parse(data, false, false).cssRules; + $scope.classes.splice(0, 0, 'noclass'); + }); + assetsService.loadCss('/App_Plugins/Lecoati.uSky.Grid/lib/uSky.Grid.ApprovedColorPicker.css'); + assetsService.loadCss(cssPath); + }); + }); + function ContentEditDialogController($scope, editorState, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, umbModelMapper, $http) { + $scope.defaultButton = null; + $scope.subButtons = []; + var dialogOptions = $scope.$parent.dialogOptions; + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish + function performSave(args) { + contentEditingHelper.contentEditorPerformSave({ + statusMessage: args.statusMessage, + saveMethod: args.saveMethod, + scope: $scope, + content: $scope.content + }).then(function (content) { + //success + if (dialogOptions.closeOnSave) { + $scope.submit(content); + } + }, function (err) { }); } - - return entity; - }; - - function init(content) { - var buttons = contentEditingHelper.configureContentEditorButtons({ - create: $routeParams.create, - content: content, - methods: { - saveAndPublish: $scope.saveAndPublish, - sendToPublish: $scope.sendToPublish, - save: $scope.save, - unPublish: angular.noop + function filterTabs(entity, blackList) { + if (blackList) { + _.each(entity.tabs, function (tab) { + tab.hide = _.contains(blackList, tab.alias); + }); } - }); - $scope.defaultButton = buttons.defaultButton; - $scope.subButtons = buttons.subButtons; - - //This is a total hack but we have really no other way of sharing data to the property editors of this - // content item, so we'll just set the property on the content item directly - $scope.content.isDialogEditor = true; - - editorState.set($scope.content); - } - - //check if the entity is being passed in, otherwise load it from the server - if (angular.isObject(dialogOptions.entity)) { - $scope.loaded = true; - $scope.content = filterTabs(dialogOptions.entity, dialogOptions.tabFilter); - init($scope.content); - } - else { - contentResource.getById(dialogOptions.id) - .then(function(data) { + return entity; + } + ; + function init(content) { + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $routeParams.create, + content: content, + methods: { + saveAndPublish: $scope.saveAndPublish, + sendToPublish: $scope.sendToPublish, + save: $scope.save, + unPublish: angular.noop + } + }); + $scope.defaultButton = buttons.defaultButton; + $scope.subButtons = buttons.subButtons; + //This is a total hack but we have really no other way of sharing data to the property editors of this + // content item, so we'll just set the property on the content item directly + $scope.content.isDialogEditor = true; + editorState.set($scope.content); + } + //check if the entity is being passed in, otherwise load it from the server + if (angular.isObject(dialogOptions.entity)) { + $scope.loaded = true; + $scope.content = filterTabs(dialogOptions.entity, dialogOptions.tabFilter); + init($scope.content); + } else { + contentResource.getById(dialogOptions.id).then(function (data) { $scope.loaded = true; $scope.content = filterTabs(data, dialogOptions.tabFilter); init($scope.content); @@ -612,109 +513,84 @@ function ContentEditDialogController($scope, editorState, $routeParams, $q, $tim // if there are any and then clear them so the collection no longer persists them. serverValidationManager.executeAndClearAllSubscriptions(); }); - } - - $scope.sendToPublish = function () { - performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending..." }); - }; - - $scope.saveAndPublish = function () { - performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing..." }); - }; - - $scope.save = function () { - performSave({ saveMethod: contentResource.save, statusMessage: "Saving..." }); - }; - - // this method is called for all action buttons and then we proxy based on the btn definition - $scope.performAction = function (btn) { - - if (!btn || !angular.isFunction(btn.handler)) { - throw "btn.handler must be a function reference"; - } - - if (!$scope.busy) { - btn.handler.apply(this); - } - }; - -} - - -angular.module("umbraco") - .controller("Umbraco.Dialogs.Content.EditController", ContentEditDialogController); -angular.module("umbraco") - .controller("Umbraco.Dialogs.HelpController", function ($scope, $location, $routeParams, helpService, userService, localizationService) { + } + $scope.sendToPublish = function () { + performSave({ + saveMethod: contentResource.sendToPublish, + statusMessage: 'Sending...' + }); + }; + $scope.saveAndPublish = function () { + performSave({ + saveMethod: contentResource.publish, + statusMessage: 'Publishing...' + }); + }; + $scope.save = function () { + performSave({ + saveMethod: contentResource.save, + statusMessage: 'Saving...' + }); + }; + // this method is called for all action buttons and then we proxy based on the btn definition + $scope.performAction = function (btn) { + if (!btn || !angular.isFunction(btn.handler)) { + throw 'btn.handler must be a function reference'; + } + if (!$scope.busy) { + btn.handler.apply(this); + } + }; + } + angular.module('umbraco').controller('Umbraco.Dialogs.Content.EditController', ContentEditDialogController); + angular.module('umbraco').controller('Umbraco.Dialogs.HelpController', function ($scope, $location, $routeParams, helpService, userService, localizationService) { $scope.section = $routeParams.section; - $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - - if(!$scope.section){ - $scope.section = "content"; + $scope.version = Umbraco.Sys.ServerVariables.application.version + ' assembly: ' + Umbraco.Sys.ServerVariables.application.assemblyVersion; + if (!$scope.section) { + $scope.section = 'content'; } - $scope.sectionName = $scope.section; - var rq = {}; rq.section = $scope.section; - //translate section name - localizationService.localize("sections_" + rq.section).then(function (value) { + localizationService.localize('sections_' + rq.section).then(function (value) { $scope.sectionName = value; }); - - userService.getCurrentUser().then(function(user){ - - rq.usertype = user.userType; - rq.lang = user.locale; - - if($routeParams.url){ - rq.path = decodeURIComponent($routeParams.url); - - if(rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0){ - rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); - } - - if(rq.path.indexOf(".aspx") > 0){ - rq.path = rq.path.substring(0, rq.path.indexOf(".aspx")); - } - - }else{ - rq.path = rq.section + "/" + $routeParams.tree + "/" + $routeParams.method; - } - - helpService.findHelp(rq).then(function(topics){ - $scope.topics = topics; - }); - - helpService.findVideos(rq).then(function(videos){ - $scope.videos = videos; - }); - - }); - - - }); -//used for the icon picker dialog -angular.module("umbraco") - .controller("Umbraco.Dialogs.IconPickerController", - function ($scope, iconHelper) { - - iconHelper.getIcons().then(function(icons){ - $scope.icons = icons; + userService.getCurrentUser().then(function (user) { + rq.lang = user.locale; + if ($routeParams.url) { + rq.path = decodeURIComponent($routeParams.url); + if (rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0) { + rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); + } + if (rq.path.indexOf('.aspx') > 0) { + rq.path = rq.path.substring(0, rq.path.indexOf('.aspx')); + } + } else { + rq.path = rq.section + '/' + $routeParams.tree + '/' + $routeParams.method; + } + helpService.findHelp(rq).then(function (topics) { + $scope.topics = topics; + }); + helpService.findVideos(rq).then(function (videos) { + $scope.videos = videos; }); - - $scope.submitClass = function (icon) { - if($scope.color) { - $scope.submit(icon + " " + $scope.color); - } - else { - $scope.submit(icon); - } - }; - - } - ); -/** + }); + }); + //used for the icon picker dialog + angular.module('umbraco').controller('Umbraco.Dialogs.IconPickerController', function ($scope, iconHelper) { + iconHelper.getIcons().then(function (icons) { + $scope.icons = icons; + }); + $scope.submitClass = function (icon) { + if ($scope.color) { + $scope.submit(icon + ' ' + $scope.color); + } else { + $scope.submit(icon); + } + }; + }); + /** * @ngdoc controller * @name Umbraco.Dialogs.InsertMacroController * @function @@ -722,4358 +598,1297 @@ angular.module("umbraco") * @description * The controller for the custom insert macro dialog. Until we upgrade the template editor to be angular this * is actually loaded into an iframe with full html. - */ -function InsertMacroController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper) { - - /** changes the view to edit the params of the selected macro */ - function editParams() { - //get the macro params if there are any - macroResource.getMacroParameters($scope.selectedMacro.id) - .then(function (data) { - - //go to next page if there are params otherwise we can just exit - if (!angular.isArray(data) || data.length === 0) { - //we can just exist! - submitForm(); - - } else { - $scope.wizardStep = "paramSelect"; - $scope.macroParams = data; - - //fill in the data if we are editing this macro - if ($scope.dialogData && $scope.dialogData.macroData && $scope.dialogData.macroData.macroParamsDictionary) { - _.each($scope.dialogData.macroData.macroParamsDictionary, function (val, key) { - var prop = _.find($scope.macroParams, function (item) { - return item.alias == key; - }); - if (prop) { - - if (_.isString(val)) { - //we need to unescape values as they have most likely been escaped while inserted - val = _.unescape(val); - - //detect if it is a json string - if (val.detectIsJson()) { - try { - //Parse it to json - prop.value = angular.fromJson(val); - } - catch (e) { - // not json - prop.value = val; - } - } - else { - prop.value = val; - } - } - else { - prop.value = val; - } - } - }); - - } - } - }); - } - - /** submit the filled out macro params */ - function submitForm() { - - //collect the value data, close the dialog and send the data back to the caller - - //create a dictionary for the macro params - var paramDictionary = {}; - _.each($scope.macroParams, function (item) { - - var val = item.value; - - if (item.value != null && item.value != undefined && !_.isString(item.value)) { - try { - val = angular.toJson(val); - } - catch (e) { - // not json - } - } - - //each value needs to be xml escaped!! since the value get's stored as an xml attribute - paramDictionary[item.alias] = _.escape(val); - - }); - - //need to find the macro alias for the selected id - var macroAlias = $scope.selectedMacro.alias; - - //get the syntax based on the rendering engine - var syntax; - if ($scope.dialogData.renderingEngine && $scope.dialogData.renderingEngine === "WebForms") { - syntax = macroService.generateWebFormsSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); - } - else if ($scope.dialogData.renderingEngine && $scope.dialogData.renderingEngine === "Mvc") { - syntax = macroService.generateMvcSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); - } - else { - syntax = macroService.generateMacroSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); - } - - $scope.submit({ syntax: syntax, macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); - } - - $scope.macros = []; - $scope.selectedMacro = null; - $scope.wizardStep = "macroSelect"; - $scope.macroParams = []; - - $scope.submitForm = function () { - - if (formHelper.submitForm({ scope: $scope })) { - - formHelper.resetForm({ scope: $scope }); - - if ($scope.wizardStep === "macroSelect") { - editParams(); - } - else { - submitForm(); - } - - } - }; - - //here we check to see if we've been passed a selected macro and if so we'll set the - //editor to start with parameter editing - if ($scope.dialogData && $scope.dialogData.macroData) { - $scope.wizardStep = "paramSelect"; - } - - //get the macro list - pass in a filter if it is only for rte - entityResource.getAll("Macro", ($scope.dialogData && $scope.dialogData.richTextEditor && $scope.dialogData.richTextEditor === true) ? "UseInEditor=true" : null) - .then(function (data) { - - //if 'allowedMacros' is specified, we need to filter - if (angular.isArray($scope.dialogData.allowedMacros) && $scope.dialogData.allowedMacros.length > 0) { - $scope.macros = _.filter(data, function(d) { - return _.contains($scope.dialogData.allowedMacros, d.alias); - }); - } - else { - $scope.macros = data; - } - - - //check if there's a pre-selected macro and if it exists - if ($scope.dialogData && $scope.dialogData.macroData && $scope.dialogData.macroData.macroAlias) { - var found = _.find(data, function (item) { - return item.alias === $scope.dialogData.macroData.macroAlias; - }); - if (found) { - //select the macro and go to next screen - $scope.selectedMacro = found; - editParams(); - return; - } - } - //we don't have a pre-selected macro so ensure the correct step is set - $scope.wizardStep = "macroSelect"; - }); - - -} - -angular.module("umbraco").controller("Umbraco.Dialogs.InsertMacroController", InsertMacroController); - -/** + */ + function InsertMacroController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper) { + /** changes the view to edit the params of the selected macro */ + function editParams() { + //get the macro params if there are any + macroResource.getMacroParameters($scope.selectedMacro.id).then(function (data) { + //go to next page if there are params otherwise we can just exit + if (!angular.isArray(data) || data.length === 0) { + //we can just exist! + submitForm(); + } else { + $scope.wizardStep = 'paramSelect'; + $scope.macroParams = data; + //fill in the data if we are editing this macro + if ($scope.dialogData && $scope.dialogData.macroData && $scope.dialogData.macroData.macroParamsDictionary) { + _.each($scope.dialogData.macroData.macroParamsDictionary, function (val, key) { + var prop = _.find($scope.macroParams, function (item) { + return item.alias == key; + }); + if (prop) { + if (_.isString(val)) { + //we need to unescape values as they have most likely been escaped while inserted + val = _.unescape(val); + //detect if it is a json string + if (val.detectIsJson()) { + try { + //Parse it to json + prop.value = angular.fromJson(val); + } catch (e) { + // not json + prop.value = val; + } + } else { + prop.value = val; + } + } else { + prop.value = val; + } + } + }); + } + } + }); + } + /** submit the filled out macro params */ + function submitForm() { + //collect the value data, close the dialog and send the data back to the caller + //create a dictionary for the macro params + var paramDictionary = {}; + _.each($scope.macroParams, function (item) { + var val = item.value; + if (item.value != null && item.value != undefined && !_.isString(item.value)) { + try { + val = angular.toJson(val); + } catch (e) { + } + } + //each value needs to be xml escaped!! since the value get's stored as an xml attribute + paramDictionary[item.alias] = _.escape(val); + }); + //need to find the macro alias for the selected id + var macroAlias = $scope.selectedMacro.alias; + //get the syntax based on the rendering engine + var syntax; + if ($scope.dialogData.renderingEngine && $scope.dialogData.renderingEngine === 'WebForms') { + syntax = macroService.generateWebFormsSyntax({ + macroAlias: macroAlias, + macroParamsDictionary: paramDictionary + }); + } else if ($scope.dialogData.renderingEngine && $scope.dialogData.renderingEngine === 'Mvc') { + syntax = macroService.generateMvcSyntax({ + macroAlias: macroAlias, + macroParamsDictionary: paramDictionary + }); + } else { + syntax = macroService.generateMacroSyntax({ + macroAlias: macroAlias, + macroParamsDictionary: paramDictionary + }); + } + $scope.submit({ + syntax: syntax, + macroAlias: macroAlias, + macroParamsDictionary: paramDictionary + }); + } + $scope.macros = []; + $scope.selectedMacro = null; + $scope.wizardStep = 'macroSelect'; + $scope.macroParams = []; + $scope.submitForm = function () { + if (formHelper.submitForm({ scope: $scope })) { + formHelper.resetForm({ scope: $scope }); + if ($scope.wizardStep === 'macroSelect') { + editParams(); + } else { + submitForm(); + } + } + }; + //here we check to see if we've been passed a selected macro and if so we'll set the + //editor to start with parameter editing + if ($scope.dialogData && $scope.dialogData.macroData) { + $scope.wizardStep = 'paramSelect'; + } + //get the macro list - pass in a filter if it is only for rte + entityResource.getAll('Macro', $scope.dialogData && $scope.dialogData.richTextEditor && $scope.dialogData.richTextEditor === true ? 'UseInEditor=true' : null).then(function (data) { + //if 'allowedMacros' is specified, we need to filter + if (angular.isArray($scope.dialogData.allowedMacros) && $scope.dialogData.allowedMacros.length > 0) { + $scope.macros = _.filter(data, function (d) { + return _.contains($scope.dialogData.allowedMacros, d.alias); + }); + } else { + $scope.macros = data; + } + //check if there's a pre-selected macro and if it exists + if ($scope.dialogData && $scope.dialogData.macroData && $scope.dialogData.macroData.macroAlias) { + var found = _.find(data, function (item) { + return item.alias === $scope.dialogData.macroData.macroAlias; + }); + if (found) { + //select the macro and go to next screen + $scope.selectedMacro = found; + editParams(); + return; + } + } + //we don't have a pre-selected macro so ensure the correct step is set + $scope.wizardStep = 'macroSelect'; + }); + } + angular.module('umbraco').controller('Umbraco.Dialogs.InsertMacroController', InsertMacroController); + /** * @ngdoc controller * @name Umbraco.Dialogs.LegacyDeleteController * @function * * @description * The controller for deleting content - */ -function LegacyDeleteController($scope, legacyResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - - legacyResource.deleteItem({ - nodeId: $scope.currentNode.id, - nodeType: $scope.currentNode.nodeType, - alias: $scope.currentNode.name, - }).then(function () { - $scope.currentNode.loading = false; - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Dialogs.LegacyDeleteController", LegacyDeleteController); - -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { - var dialogOptions = $scope.dialogOptions; - - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - $scope.dialogTreeEventHandler = $({}); - $scope.target = {}; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - if (dialogOptions.currentTarget) { - $scope.target = dialogOptions.currentTarget; - - //if we have a node ID, we fetch the current node to build the form data - if ($scope.target.id || $scope.target.udi) { - - var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; - - if (!$scope.target.path) { - entityResource.getPath(id, "Document").then(function (path) { - $scope.target.path = path; - //now sync the tree to this path - $scope.dialogTreeEventHandler.syncTree({ path: $scope.target.path, tree: "content" }); - }); - } - - contentResource.getNiceUrl(id).then(function (url) { - $scope.target.url = url; - }); - } - } - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.listViewNode) { - //check if list view 'search' node was selected - - $scope.searchInfo.showSearch = true; - $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; - $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; - } - else { - eventsService.emit("dialogs.linkPicker.select", args); - - if ($scope.currentNode) { - //un-select if there's a current one selected - $scope.currentNode.selected = false; - } - - $scope.currentNode = args.node; - $scope.currentNode.selected = true; - $scope.target.id = args.node.id; - $scope.target.udi = args.node.udi; - $scope.target.name = args.node.name; - - if (args.node.id < 0) { - $scope.target.url = "/"; - } - else { - contentResource.getNiceUrl(args.node.id).then(function (url) { - $scope.target.url = url; - }); - } - - if (!angular.isUndefined($scope.target.isMedia)) { - delete $scope.target.isMedia; - } - } - } - - function nodeExpandedHandler(ev, args) { - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - //check if any of the items are list views, if so we need to add a custom - // child: A node to activate the search - if (child.metaData.isContainer) { - child.hasChildren = true; - child.children = [ - { - level: child.level + 1, - hasChildren: false, - name: searchText, - metaData: { - listViewNode: child, - }, - cssClass: "icon umb-tree-icon sprTree icon-search", - cssClasses: ["not-published"] - } - ]; - } - }); - } - } - - $scope.switchToMediaPicker = function () { - userService.getCurrentUser().then(function (userData) { - dialogService.mediaPicker({ - startNodeId: userData.startMediaId, - callback: function (media) { - $scope.target.id = media.id; - $scope.target.isMedia = true; - $scope.target.name = media.name; - $scope.target.url = mediaHelper.resolveFile(media); - } - }); - }); - }; - - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, {event: evt, node: result}); - }; - - //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; - $scope.searchInfo.showSearch = true; - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - }); -angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", - function ($scope, $cookies, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService) { - - var setFieldFocus = function(form, field) { - $timeout(function() { - $("form[name='" + form + "'] input[name='" + field + "']").focus(); - }); - } - - var twoFactorloginDialog = null; - function show2FALoginDialog(view, callback) { - if (!twoFactorloginDialog) { - twoFactorloginDialog = dialogService.open({ - - //very special flag which means that global events cannot close this dialog - manualClose: true, - template: view, - modalClass: "login-overlay", - animation: "slide", - show: true, - callback: callback, - - }); - } - } - - function resetInputValidation() { - $scope.confirmPassword = ""; - $scope.password = ""; - $scope.login = ""; - if ($scope.loginForm) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - if ($scope.requestPasswordResetForm) { - $scope.requestPasswordResetForm.email.$setValidity("auth", true); - } - if ($scope.setPasswordForm) { - $scope.setPasswordForm.password.$setValidity('auth', true); - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - } - - $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; - - $scope.showLogin = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "login"; - setFieldFocus("loginForm", "username"); - } - - $scope.showRequestPasswordReset = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "request-password-reset"; - $scope.showEmailResetConfirmation = false; - setFieldFocus("requestPasswordResetForm", "email"); - } - - $scope.showSetPassword = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "set-password"; - setFieldFocus("setPasswordForm", "password"); - } - - var d = new Date(); - var konamiGreetings = new Array("Suze Sunday", "Malibu Monday", "Tequila Tuesday", "Whiskey Wednesday", "Negroni Day", "Fernet Friday", "Sancerre Saturday"); - var konamiMode = $cookies.konamiLogin; - if (konamiMode == "1") { - $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; - } else { - localizationService.localize("login_greeting" + d.getDay()).then(function (label) { - $scope.greeting = label; - }); // weekday[d.getDay()]; - } - $scope.errorMsg = ""; - - $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; - $scope.externalLoginProviders = externalLoginInfo.providers; - $scope.externalLoginInfo = externalLoginInfo; - $scope.resetPasswordCodeInfo = resetPasswordCodeInfo; - $scope.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; - - $scope.activateKonamiMode = function () { - if ($cookies.konamiLogin == "1") { - // somehow I can't update the cookie value using $cookies, so going native - document.cookie = "konamiLogin=; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; - document.location.reload(); - } else { - document.cookie = "konamiLogin=1; expires=Tue, 01 Jan 2030 00:00:01 GMT;"; - $scope.$apply(function () { - $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; - }); - } - } - - $scope.loginSubmit = function (login, password) { - - //if the login and password are not empty we need to automatically - // validate them - this is because if there are validation errors on the server - // then the user has to change both username & password to resubmit which isn't ideal, - // so if they're not empty, we'll just make sure to set them to valid. - if (login && password && login.length > 0 && password.length > 0) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - - if ($scope.loginForm.$invalid) { - return; - } - - userService.authenticate(login, password) - .then(function(data) { - $scope.submit(true); - }, - function(reason) { - - //is Two Factor required? - if (reason.status === 402) { - $scope.errorMsg = "Additional authentication required"; - show2FALoginDialog(reason.data.twoFactorView, $scope.submit); - } - else { - $scope.errorMsg = reason.errorMsg; - - //set the form inputs to invalid - $scope.loginForm.username.$setValidity("auth", false); - $scope.loginForm.password.$setValidity("auth", false); - } - }); - - //setup a watch for both of the model values changing, if they change - // while the form is invalid, then revalidate them so that the form can - // be submitted again. - $scope.loginForm.username.$viewChangeListeners.push(function () { - if ($scope.loginForm.username.$invalid) { - $scope.loginForm.username.$setValidity('auth', true); - } - }); - $scope.loginForm.password.$viewChangeListeners.push(function () { - if ($scope.loginForm.password.$invalid) { - $scope.loginForm.password.$setValidity('auth', true); - } - }); - }; - - $scope.requestPasswordResetSubmit = function (email) { - - if (email && email.length > 0) { - $scope.requestPasswordResetForm.email.$setValidity('auth', true); - } - - $scope.showEmailResetConfirmation = false; - - if ($scope.requestPasswordResetForm.$invalid) { - return; - } - - $scope.errorMsg = ""; - - authResource.performRequestPasswordReset(email) - .then(function () { - //remove the email entered - $scope.email = ""; - $scope.showEmailResetConfirmation = true; - }, function (reason) { - $scope.errorMsg = reason.errorMsg; - $scope.requestPasswordResetForm.email.$setValidity("auth", false); - }); - - $scope.requestPasswordResetForm.email.$viewChangeListeners.push(function () { - if ($scope.requestPasswordResetForm.email.$invalid) { - $scope.requestPasswordResetForm.email.$setValidity('auth', true); - } - }); - }; - - $scope.setPasswordSubmit = function (password, confirmPassword) { - - $scope.showSetPasswordConfirmation = false; - - if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { - $scope.setPasswordForm.password.$setValidity('auth', true); - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - - if ($scope.setPasswordForm.$invalid) { - return; - } - - authResource.performSetPassword($scope.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, $scope.resetPasswordCodeInfo.resetCodeModel.resetCode) - .then(function () { - $scope.showSetPasswordConfirmation = true; - $scope.resetComplete = true; - - //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again - resetPasswordCodeInfo.resetCodeModel = null; - - }, function (reason) { - if (reason.data && reason.data.Message) { - $scope.errorMsg = reason.data.Message; - } - else { - $scope.errorMsg = reason.errorMsg; - } - $scope.setPasswordForm.password.$setValidity("auth", false); - $scope.setPasswordForm.confirmPassword.$setValidity("auth", false); - }); - - $scope.setPasswordForm.password.$viewChangeListeners.push(function () { - if ($scope.setPasswordForm.password.$invalid) { - $scope.setPasswordForm.password.$setValidity('auth', true); - } - }); - $scope.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { - if ($scope.setPasswordForm.confirmPassword.$invalid) { - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - }); - } - - - //Now, show the correct panel: - - if ($scope.resetPasswordCodeInfo.resetCodeModel) { - $scope.showSetPassword(); - } - else if ($scope.resetPasswordCodeInfo.errors.length > 0) { - $scope.view = "password-reset-code-expired"; - } - else { - $scope.showLogin(); - } - - }); - -//used for the macro picker dialog -angular.module("umbraco").controller("Umbraco.Dialogs.MacroPickerController", function ($scope, macroFactory, umbPropEditorHelper) { - $scope.macros = macroFactory.all(true); - $scope.dialogMode = "list"; - - $scope.configureMacro = function(macro){ - $scope.dialogMode = "configure"; - $scope.dialogData.macro = macroFactory.getMacro(macro.alias); - //set the correct view for each item - for (var i = 0; i < dialogData.macro.properties.length; i++) { - dialogData.macro.properties[i].editorView = umbPropEditorHelper.getViewPath(dialogData.macro.properties[i].view); - } - }; -}); -//used for the media picker dialog -angular.module("umbraco") - .controller("Umbraco.Dialogs.MediaPickerController", - function($scope, mediaResource, umbRequestHelper, entityResource, $log, mediaHelper, mediaTypeHelper, eventsService, treeService) { - - var dialogOptions = $scope.dialogOptions; - - $scope.onlyImages = dialogOptions.onlyImages; - $scope.showDetails = dialogOptions.showDetails; - $scope.multiPicker = (dialogOptions.multiPicker && dialogOptions.multiPicker !== "0") ? true : false; - $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; - $scope.cropSize = dialogOptions.cropSize; - - //preload selected item - $scope.target = undefined; - if (dialogOptions.currentTarget) { - $scope.target = dialogOptions.currentTarget; - } - - $scope.acceptedMediatypes = []; - mediaTypeHelper.getAllowedImagetypes($scope.startNodeId) - .then(function(types) { - $scope.acceptedMediatypes = types; - }); - - $scope.upload = function(v) { - angular.element(".umb-file-dropzone-directive .file-select").click(); - }; - - $scope.dragLeave = function(el, event) { - $scope.activeDrag = false; - }; - - $scope.dragEnter = function(el, event) { - $scope.activeDrag = true; - }; - - $scope.submitFolder = function(e) { - if (e.keyCode === 13) { - e.preventDefault(); - - mediaResource - .addFolder($scope.newFolderName, $scope.currentFolder.id) - .then(function(data) { - $scope.showFolderInput = false; - $scope.newFolderName = ""; - - //we've added a new folder so lets clear the tree cache for that specific item - treeService.clearCache({ - cacheKey: "__media", //this is the main media tree cache key - childrenOf: data.parentId //clear the children of the parent - }); - - $scope.gotoFolder(data); - }); - } - }; - - $scope.gotoFolder = function(folder) { - if (!folder) { - folder = { id: -1, name: "Media", icon: "icon-folder" }; - } - - if (folder.id > 0) { - entityResource.getAncestors(folder.id, "media") - .then(function(anc) { - // anc.splice(0,1); - $scope.path = _.filter(anc, - function(f) { - return f.path.indexOf($scope.startNodeId) !== -1; - }); - }); - - mediaTypeHelper.getAllowedImagetypes(folder.id) - .then(function(types) { - $scope.acceptedMediatypes = types; - }); - } else { - $scope.path = []; - } - - //mediaResource.rootMedia() - mediaResource.getChildren(folder.id) - .then(function(data) { - $scope.searchTerm = ""; - $scope.images = data.items ? data.items : []; - }); - - $scope.currentFolder = folder; - }; - - - $scope.clickHandler = function(image, ev, select) { - ev.preventDefault(); - - if (image.isFolder && !select) { - $scope.gotoFolder(image); - } else { - eventsService.emit("dialogs.mediaPicker.select", image); - - //we have 3 options add to collection (if multi) show details, or submit it right back to the callback - if ($scope.multiPicker) { - $scope.select(image); - image.cssclass = ($scope.dialogData.selection.indexOf(image) > -1) ? "selected" : ""; - } else if ($scope.showDetails) { - $scope.target = image; - $scope.target.url = mediaHelper.resolveFile(image); - } else { - $scope.submit(image); - } - } - }; - - $scope.exitDetails = function() { - if (!$scope.currentFolder) { - $scope.gotoFolder(); - } - - $scope.target = undefined; - }; - - $scope.onUploadComplete = function() { - $scope.gotoFolder($scope.currentFolder); - }; - - $scope.onFilesQueue = function() { - $scope.activeDrag = false; - }; - - //default root item - if (!$scope.target) { - $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); - } - }); -//used for the member picker dialog -angular.module("umbraco").controller("Umbraco.Dialogs.MemberGroupPickerController", - function($scope, eventsService, entityResource, searchService, $log) { - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - $scope.multiPicker = dialogOptions.multiPicker; - - /** Method used for selecting a node */ - function select(text, id) { - - if (dialogOptions.multiPicker) { - $scope.select(id); - } - else { - $scope.submit(id); - } - } - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - eventsService.emit("dialogs.memberGroupPicker.select", args); - - //This is a tree node, so we don't have an entity to pass in, it will need to be looked up - //from the server in this method. - select(args.node.name, args.node.id); - - //toggle checked state - args.node.selected = args.node.selected === true ? false : true; - } - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); -angular.module("umbraco").controller("Umbraco.Dialogs.RteEmbedController", function ($scope, $http, umbRequestHelper) { - $scope.form = {}; - $scope.form.url = ""; - $scope.form.width = 360; - $scope.form.height = 240; - $scope.form.constrain = true; - $scope.form.preview = ""; - $scope.form.success = false; - $scope.form.info = ""; - $scope.form.supportsDimensions = false; - - var origWidth = 500; - var origHeight = 300; - - $scope.showPreview = function() { - - if ($scope.form.url) { - $scope.form.show = true; - $scope.form.preview = "
"; - $scope.form.info = ""; - $scope.form.success = false; - - $http({ method: 'GET', url: umbRequestHelper.getApiUrl("embedApiBaseUrl", "GetEmbed"), params: { url: $scope.form.url, width: $scope.form.width, height: $scope.form.height } }) - .success(function (data) { - - $scope.form.preview = ""; - - switch (data.Status) { - case 0: - //not supported - $scope.form.info = "Not supported"; - break; - case 1: - //error - $scope.form.info = "Could not embed media - please ensure the URL is valid"; - break; - case 2: - $scope.form.preview = data.Markup; - $scope.form.supportsDimensions = data.SupportsDimensions; - $scope.form.success = true; - break; - } - }) - .error(function () { - $scope.form.supportsDimensions = false; - $scope.form.preview = ""; - $scope.form.info = "Could not embed media - please ensure the URL is valid"; - }); - } else { - $scope.form.supportsDimensions = false; - $scope.form.preview = ""; - $scope.form.info = "Please enter a URL"; - } - }; - - $scope.changeSize = function (type) { - var width, height; - - if ($scope.form.constrain) { - width = parseInt($scope.form.width, 10); - height = parseInt($scope.form.height, 10); - if (type == 'width') { - origHeight = Math.round((width / origWidth) * height); - $scope.form.height = origHeight; - } else { - origWidth = Math.round((height / origHeight) * width); - $scope.form.width = origWidth; - } - } - if ($scope.form.url != "") { - $scope.showPreview(); - } - - }; - - $scope.insert = function(){ - $scope.submit($scope.form.preview); - }; -}); -angular.module("umbraco").controller('Umbraco.Dialogs.Template.QueryBuilderController', - function($scope, $http, dialogService){ - - - $http.get("backoffice/UmbracoApi/TemplateQuery/GetAllowedProperties").then(function(response) { - $scope.properties = response.data; - }); - - $http.get("backoffice/UmbracoApi/TemplateQuery/GetContentTypes").then(function (response) { - $scope.contentTypes = response.data; - }); - - $http.get("backoffice/UmbracoApi/TemplateQuery/GetFilterConditions").then(function (response) { - $scope.conditions = response.data; + */ + function LegacyDeleteController($scope, legacyResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + legacyResource.deleteItem({ + nodeId: $scope.currentNode.id, + nodeType: $scope.currentNode.nodeType, + alias: $scope.currentNode.name + }).then(function () { + $scope.currentNode.loading = false; + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); }); - - - $scope.query = { - contentType: { - name: "Everything" - }, - source:{ - name: "My website" - }, - filters:[ - { - property:undefined, - operator: undefined - } - ], - sort:{ - property:{ - alias: "", - name: "", - }, - direction: "ascending" - } - }; - - - - $scope.chooseSource = function(query){ - dialogService.contentPicker({ - callback: function (data) { - - if (data.id > 0) { - query.source = { id: data.id, name: data.name }; - } else { - query.source.name = "My website"; - delete query.source.id; - } - } - }); - }; - - var throttledFunc = _.throttle(function() { - - $http.post("backoffice/UmbracoApi/TemplateQuery/PostTemplateQuery", $scope.query).then(function (response) { - $scope.result = response.data; - }); - - }, 200); - - $scope.$watch("query", function(value) { - throttledFunc(); - }, true); - - $scope.getPropertyOperators = function (property) { - - var conditions = _.filter($scope.conditions, function(condition) { - var index = condition.appliesTo.indexOf(property.type); - return index >= 0; - }); - return conditions; - }; - - - $scope.addFilter = function(query){ - query.filters.push({}); - }; - - $scope.trashFilter = function (query) { - query.filters.splice(query,1); - }; - - $scope.changeSortOrder = function(query){ - if(query.sort.direction === "ascending"){ - query.sort.direction = "descending"; - }else{ - query.sort.direction = "ascending"; - } - }; - - $scope.setSortProperty = function(query, property){ - query.sort.property = property; - if(property.type === "datetime"){ - query.sort.direction = "descending"; - }else{ - query.sort.direction = "ascending"; - } - }; - }); -angular.module("umbraco").controller('Umbraco.Dialogs.Template.SnippetController', - function($scope) { - $scope.type = $scope.dialogOptions.type; - $scope.section = { - name: "", - required: false - }; - }); -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Dialogs.TreePickerController", - function ($scope, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService) { - - var tree = null; - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - $scope.section = dialogOptions.section; - $scope.treeAlias = dialogOptions.treeAlias; - $scope.multiPicker = dialogOptions.multiPicker; - $scope.hideHeader = true; - $scope.searchInfo = { - searchFromId: dialogOptions.startNodeId, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - //create the custom query string param for this tree - $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; - $scope.customTreeParams += dialogOptions.customTreeParams ? "&" + dialogOptions.customTreeParams : ""; - - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - // Allow the entity type to be passed in but defaults to Document for backwards compatibility. - var entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; - - - //min / max values - if (dialogOptions.minNumber) { - dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); - } - if (dialogOptions.maxNumber) { - dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); - } - - if (dialogOptions.section === "member") { - entityType = "Member"; - } - else if (dialogOptions.section === "media") { - entityType = "Media"; - } - - //Configures filtering - if (dialogOptions.filter) { - - dialogOptions.filterExclude = false; - dialogOptions.filterAdvanced = false; - - //used advanced filtering - if (angular.isFunction(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else if (angular.isObject(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else { - if (dialogOptions.filter.startsWith("!")) { - dialogOptions.filterExclude = true; - dialogOptions.filter = dialogOptions.filter.substring(1); - } - - //used advanced filtering - if (dialogOptions.filter.startsWith("{")) { - dialogOptions.filterAdvanced = true; - //convert to object - dialogOptions.filter = angular.fromJson(dialogOptions.filter); - } - } - } - - function nodeExpandedHandler(ev, args) { - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - - //check if any of the items are list views, if so we need to add some custom - // children: A node to activate the search, any nodes that have already been - // selected in the search - if (child.metaData.isContainer) { - child.hasChildren = true; - child.children = [ - { - level: child.level + 1, - hasChildren: false, - parent: function () { - return child; - }, - name: searchText, - metaData: { - listViewNode: child, - }, - cssClass: "icon-search", - cssClasses: ["not-published"] - } - ]; - //add base transition classes to this node - child.cssClasses.push("tree-node-slide-up"); - - var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function(i) { - return i.parentId == child.id; - }); - _.each(listViewResults, function(item) { - child.children.unshift({ - id: item.id, - name: item.name, - cssClass: "icon umb-tree-icon sprTree " + item.icon, - level: child.level + 1, - metaData: { - isSearchResult: true - }, - hasChildren: false, - parent: function () { - return child; - } - }); - }); - } - - //now we need to look in the already selected search results and - // toggle the check boxes for those ones that are listed - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - if (exists) { - child.selected = true; - } - }); - - //check filter - performFiltering(args.children); - } - } - - //gets the tree object when it loads - function treeLoadedHandler(ev, args) { - tree = args.tree; - } - - //wires up selection - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.listViewNode) { - //check if list view 'search' node was selected - - $scope.searchInfo.showSearch = true; - $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; - $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; - - //add transition classes - var listViewNode = args.node.parent(); - listViewNode.cssClasses.push('tree-node-slide-up-hide-active'); - } - else if (args.node.metaData.isSearchResult) { - //check if the item selected was a search result from a list view - - //unselect - select(args.node.name, args.node.id); - - //remove it from the list view children - var listView = args.node.parent(); - listView.children = _.reject(listView.children, function(child) { - return child.id == args.node.id; - }); - - //remove it from the custom tracked search result list - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { - return i.id == args.node.id; - }); - } - else { - eventsService.emit("dialogs.treePickerController.select", args); - - if (args.node.filtered) { - return; - } - - //This is a tree node, so we don't have an entity to pass in, it will need to be looked up - //from the server in this method. - select(args.node.name, args.node.id); - - //toggle checked state - args.node.selected = args.node.selected === true ? false : true; - } - } - - /** Method used for selecting a node */ - function select(text, id, entity) { - //if we get the root, we just return a constructed entity, no need for server data - if (id < 0) { - if ($scope.multiPicker) { - $scope.select(id); - } - else { - var node = { - alias: null, - icon: "icon-folder", - id: id, - name: text - }; - $scope.submit(node); - } - } - else { - - if ($scope.multiPicker) { - $scope.select(Number(id)); - } - else { - - $scope.hideSearch(); - - //if an entity has been passed in, use it - if (entity) { - $scope.submit(entity); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, entityType).then(function (ent) { - $scope.submit(ent); - }); - } - } - } - } - - function performFiltering(nodes) { - - if (!dialogOptions.filter) { - return; - } - - //remove any list view search nodes from being filtered since these are special nodes that always must - // be allowed to be clicked on - nodes = _.filter(nodes, function(n) { - return !angular.isObject(n.metaData.listViewNode); - }); - - if (dialogOptions.filterAdvanced) { - - //filter either based on a method or an object - var filtered = angular.isFunction(dialogOptions.filter) - ? _.filter(nodes, dialogOptions.filter) - : _.where(nodes, dialogOptions.filter); - - angular.forEach(filtered, function (value, key) { - value.filtered = true; - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - }); - } else { - var a = dialogOptions.filter.toLowerCase().replace(/\s/g, '').split(','); - angular.forEach(nodes, function (value, key) { - - var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; - - if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { - value.filtered = true; - - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - } - }); - } - } - - $scope.multiSubmit = function (result) { - entityResource.getByIds(result, entityType).then(function (ents) { - $scope.submit(ents); - }); - }; - - /** method to select a search result */ - $scope.selectResult = function (evt, result) { - - if (result.filtered) { - return; - } - - result.selected = result.selected === true ? false : true; - - //since result = an entity, we'll pass it in so we don't have to go back to the server - select(result.name, result.id, result); - - //add/remove to our custom tracked list of selected search results - if (result.selected) { - $scope.searchInfo.selectedSearchResults.push(result); - } - else { - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function(i) { - return i.id == result.id; - }); - } - - //ensure the tree node in the tree is checked/unchecked if it already exists there - if (tree) { - var found = treeService.getDescendantNode(tree.root, result.id); - if (found) { - found.selected = result.selected; - } - } - - }; - - $scope.hideSearch = function () { - - //Traverse the entire displayed tree and update each node to sync with the selected search results - if (tree) { - - //we need to ensure that any currently displayed nodes that get selected - // from the search get updated to have a check box! - function checkChildren(children) { - _.each(children, function (child) { - //check if the id is in the selection, if so ensure it's flagged as selected - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - //if the curr node exists in selected search results, ensure it's checked - if (exists) { - child.selected = true; - } - //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result - else if (child.metaData.isSearchResult) { - //if this tree node is under a list view it means that the node was added - // to the tree dynamically under the list view that was searched, so we actually want to remove - // it all together from the tree - var listView = child.parent(); - listView.children = _.reject(listView.children, function(c) { - return c.id == child.id; - }); - } - - //check if the current node is a list view and if so, check if there's any new results - // that need to be added as child nodes to it based on search results selected - if (child.metaData.isContainer) { - - child.cssClasses = _.reject(child.cssClasses, function(c) { - return c === 'tree-node-slide-up-hide-active'; - }); - - var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { - return i.parentId == child.id; - }); - _.each(listViewResults, function (item) { - var childExists = _.find(child.children, function(c) { - return c.id == item.id; - }); - if (!childExists) { - var parent = child; - child.children.unshift({ - id: item.id, - name: item.name, - cssClass: "icon umb-tree-icon sprTree " + item.icon, - level: child.level + 1, - metaData: { - isSearchResult: true - }, - hasChildren: false, - parent: function () { - return parent; - } - }); - } - }); - } - - //recurse - if (child.children && child.children.length > 0) { - checkChildren(child.children); - } - }); - } - checkChildren(tree.root.children); - } - - - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = dialogOptions.startNodeId; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - $scope.onSearchResults = function(results) { - - //filter all items - this will mark an item as filtered - performFiltering(results); - - //now actually remove all filtered items so they are not even displayed - results = _.filter(results, function(item) { - return !item.filtered; - }); - - $scope.searchInfo.results = results; - - //sync with the curr selected results - _.each($scope.searchInfo.results, function (result) { - var exists = _.find($scope.dialogData.selection, function (selectedId) { - return result.id == selectedId; - }); - if (exists) { - result.selected = true; - } - }); - - $scope.searchInfo.showSearch = true; - }; - - $scope.dialogTreeEventHandler.bind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); -angular.module("umbraco") - .controller("Umbraco.Dialogs.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper) { - - $scope.history = historyService.getCurrent(); - $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - $scope.showPasswordFields = false; - $scope.changePasswordButtonState = "init"; - - $scope.externalLoginProviders = externalLoginInfo.providers; - $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; - var evts = []; - evts.push(eventsService.on("historyService.add", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.remove", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.removeAll", function (e, args) { - $scope.history = []; - })); - - $scope.logout = function () { - - //Add event listener for when there are pending changes on an editor which means our route was not successful - var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { - //one time listener, remove the event - pendingChangeEvent(); - $scope.close(); - }); - - - //perform the path change, if it is successful then the promise will resolve otherwise it will fail - $scope.close(); - $location.path("/logout"); }; - - $scope.gotoHistory = function (link) { - $location.path(link); - $scope.close(); + $scope.cancel = function () { + navigationService.hideDialog(); }; - - //Manually update the remaining timeout seconds - function updateTimeout() { - $timeout(function () { - if ($scope.remainingAuthSeconds > 0) { - $scope.remainingAuthSeconds--; - $scope.$digest(); - //recurse - updateTimeout(); + } + angular.module('umbraco').controller('Umbraco.Dialogs.LegacyDeleteController', LegacyDeleteController); + //used for the media picker dialog + angular.module('umbraco').controller('Umbraco.Dialogs.LinkPickerController', function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { + var dialogOptions = $scope.dialogOptions; + var searchText = 'Search...'; + localizationService.localize('general_search').then(function (value) { + searchText = value + '...'; + }); + $scope.dialogTreeEventHandler = $({}); + $scope.target = {}; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + if (dialogOptions.currentTarget) { + $scope.target = dialogOptions.currentTarget; + //if we have a node ID, we fetch the current node to build the form data + if ($scope.target.id || $scope.target.udi) { + var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; + if (!$scope.target.path) { + entityResource.getPath(id, 'Document').then(function (path) { + $scope.target.path = path; + //now sync the tree to this path + $scope.dialogTreeEventHandler.syncTree({ + path: $scope.target.path, + tree: 'content' + }); + }); } - - }, 1000, false); // 1 second, do NOT execute a global digest + contentResource.getNiceUrl(id).then(function (url) { + $scope.target.url = url; + }); + } } - - function updateUserInfo() { - //get the user - userService.getCurrentUser().then(function (user) { - $scope.user = user; - if ($scope.user) { - $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; - //set the timer - updateTimeout(); - - authResource.getCurrentUserLinkedLogins().then(function(logins) { - //reset all to be un-linked - for (var provider in $scope.externalLoginProviders) { - $scope.externalLoginProviders[provider].linkedProviderKey = undefined; - } - - //set the linked logins - for (var login in logins) { - var found = _.find($scope.externalLoginProviders, function (i) { - return i.authType == login; - }); - if (found) { - found.linkedProviderKey = logins[login]; - } - } + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + $scope.searchInfo.showSearch = true; + $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + } else { + eventsService.emit('dialogs.linkPicker.select', args); + if ($scope.currentNode) { + //un-select if there's a current one selected + $scope.currentNode.selected = false; + } + $scope.currentNode = args.node; + $scope.currentNode.selected = true; + $scope.target.id = args.node.id; + $scope.target.udi = args.node.udi; + $scope.target.name = args.node.name; + if (args.node.id < 0) { + $scope.target.url = '/'; + } else { + contentResource.getNiceUrl(args.node.id).then(function (url) { + $scope.target.url = url; }); } - }); + if (!angular.isUndefined($scope.target.isMedia)) { + delete $scope.target.isMedia; + } + } } - - $scope.unlink = function (e, loginProvider, providerKey) { - var result = confirm("Are you sure you want to unlink this account?"); - if (!result) { - e.preventDefault(); - return; + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add a custom + // child: A node to activate the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [{ + level: child.level + 1, + hasChildren: false, + name: searchText, + metaData: { listViewNode: child }, + cssClass: 'icon umb-tree-icon sprTree icon-search', + cssClasses: ['not-published'] + }]; + } + }); } - - authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { - updateUserInfo(); - }); } - - updateUserInfo(); - - //remove all event handlers + $scope.switchToMediaPicker = function () { + userService.getCurrentUser().then(function (userData) { + dialogService.mediaPicker({ + startNodeId: userData.startMediaIds.length == 0 ? -1 : userData.startMediaIds[0], + callback: function (media) { + $scope.target.id = media.id; + $scope.target.isMedia = true; + $scope.target.name = media.name; + $scope.target.url = mediaHelper.resolveFile(media); + } + }); + }); + }; + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + }; + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { + event: evt, + node: result + }); + }; + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { - for (var e = 0; e < evts.length; e++) { - evts[e](); - } - + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); - - /* ---------- UPDATE PASSWORD ---------- */ - - //create the initial model for change password property editor - $scope.changePasswordModel = { - alias: "_umb_password", - view: "changepassword", - config: {}, - value: {} + }); + angular.module('umbraco').controller('Umbraco.Dialogs.LoginController', function ($scope, $cookies, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService, $q) { + $scope.invitedUser = null; + $scope.invitedUserPasswordModel = { + password: '', + confirmPassword: '', + buttonState: '', + passwordPolicies: null, + passwordPolicyText: '' }; - - //go get the config for the membership provider and add it to the model - currentUserResource.getMembershipProviderConfig().then(function(data) { - $scope.changePasswordModel.config = data; - //ensure the hasPassword config option is set to true (the user of course has a password already assigned) - //this will ensure the oldPassword is shown so they can change it - // disable reset password functionality beacuse it does not make sense inside the backoffice - $scope.changePasswordModel.config.hasPassword = true; - $scope.changePasswordModel.config.disableToggle = true; - $scope.changePasswordModel.config.enableReset = false; - }); - - $scope.changePassword = function() { - - if (formHelper.submitForm({ scope: $scope })) { - - $scope.changePasswordButtonState = "busy"; - - currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { - - //if the password has been reset, then update our model - if (data.value) { - $scope.changePasswordModel.value.generatedPassword = data.value; + $scope.avatarFile = { + filesHolder: null, + uploadStatus: null, + uploadProgress: 0, + maxFileSize: Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + 'KB', + acceptedFileTypes: mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes), + uploaded: false + }; + $scope.togglePassword = function () { + var elem = $('form[name=\'loginForm\'] input[name=\'password\']'); + elem.attr('type', elem.attr('type') === 'text' ? 'password' : 'text'); + }; + function init() { + // Check if it is a new user + var inviteVal = $location.search().invite; + if (inviteVal && (inviteVal === '1' || inviteVal === '2')) { + $q.all([ + //get the current invite user + authResource.getCurrentInvitedUser().then(function (data) { + $scope.invitedUser = data; + }, function () { + //it failed so we should remove the search + $location.search('invite', null); + }), + //get the membership provider config for password policies + authResource.getMembershipProviderConfig().then(function (data) { + $scope.invitedUserPasswordModel.passwordPolicies = data; + //localize the text + localizationService.localize('errorHandling_errorInPasswordFormat', [ + $scope.invitedUserPasswordModel.passwordPolicies.minPasswordLength, + $scope.invitedUserPasswordModel.passwordPolicies.minNonAlphaNumericChars + ]).then(function (data) { + $scope.invitedUserPasswordModel.passwordPolicyText = data; + }); + }) + ]).then(function () { + $scope.inviteStep = Number(inviteVal); + }); + } + } + $scope.changeAvatar = function (files, event) { + if (files && files.length > 0) { + upload(files[0]); + } + }; + $scope.getStarted = function () { + $location.search('invite', null); + $scope.submit(true); + }; + function upload(file) { + $scope.avatarFile.uploadProgress = 0; + Upload.upload({ + url: umbRequestHelper.getApiUrl('currentUserApiBaseUrl', 'PostSetAvatar'), + fields: {}, + file: file + }).progress(function (evt) { + if ($scope.avatarFile.uploadStatus !== 'done' && $scope.avatarFile.uploadStatus !== 'error') { + // set uploading status on file + $scope.avatarFile.uploadStatus = 'uploading'; + // calculate progress in percentage + var progressPercentage = parseInt(100 * evt.loaded / evt.total, 10); + // set percentage property on file + $scope.avatarFile.uploadProgress = progressPercentage; + } + }).success(function (data, status, headers, config) { + $scope.avatarFile.uploadProgress = 100; + // set done status on file + $scope.avatarFile.uploadStatus = 'done'; + $scope.invitedUser.avatars = data; + $scope.avatarFile.uploaded = true; + }).error(function (evt, status, headers, config) { + // set status done + $scope.avatarFile.uploadStatus = 'error'; + // If file not found, server will return a 404 and display this message + if (status === 404) { + $scope.avatarFile.serverErrorMessage = 'File not found'; + } else if (status == 400) { + //it's a validation error + $scope.avatarFile.serverErrorMessage = evt.message; + } else { + //it's an unhandled error + //if the service returns a detailed error + if (evt.InnerException) { + $scope.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage; + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf('ValidateRequestEntityLength') > 0) { + $scope.avatarFile.serverErrorMessage = 'File too large to upload'; + } + } else if (evt.Message) { + $scope.avatarFile.serverErrorMessage = evt.Message; } - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - $scope.changePasswordButtonState = "success"; - + } + }); + } + $scope.inviteSavePassword = function () { + if (formHelper.submitForm({ + scope: $scope, + statusMessage: 'Saving...' + })) { + $scope.invitedUserPasswordModel.buttonState = 'busy'; + currentUserResource.performSetInvitedUserPassword($scope.invitedUserPasswordModel.password).then(function (data) { + //success + formHelper.resetForm({ + scope: $scope, + notifications: data.notifications + }); + $scope.invitedUserPasswordModel.buttonState = 'success'; + //set the user and set them as logged in + $scope.invitedUser = data; + userService.setAuthenticationSuccessful(data); + $scope.inviteStep = 2; }, function (err) { - + //error formHelper.handleError(err); - - $scope.changePasswordButtonState = "error"; - + $scope.invitedUserPasswordModel.buttonState = 'error'; }); - } - }; - - $scope.togglePasswordFields = function() { - clearPasswordFields(); - $scope.showPasswordFields = !$scope.showPasswordFields; + var setFieldFocus = function (form, field) { + $timeout(function () { + $('form[name=\'' + form + '\'] input[name=\'' + field + '\']').focus(); + }); + }; + var twoFactorloginDialog = null; + function show2FALoginDialog(view, callback) { + if (!twoFactorloginDialog) { + twoFactorloginDialog = dialogService.open({ + //very special flag which means that global events cannot close this dialog + manualClose: true, + template: view, + modalClass: 'login-overlay', + animation: 'slide', + show: true, + callback: callback + }); + } } - - function clearPasswordFields() { - $scope.changePasswordModel.value.newPassword = ""; - $scope.changePasswordModel.confirm = ""; + function resetInputValidation() { + $scope.confirmPassword = ''; + $scope.password = ''; + $scope.login = ''; + if ($scope.loginForm) { + $scope.loginForm.username.$setValidity('auth', true); + $scope.loginForm.password.$setValidity('auth', true); + } + if ($scope.requestPasswordResetForm) { + $scope.requestPasswordResetForm.email.$setValidity('auth', true); + } + if ($scope.setPasswordForm) { + $scope.setPasswordForm.password.$setValidity('auth', true); + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } } - - }); - -/** - * @ngdoc controller - * @name Umbraco.Dialogs.LegacyDeleteController - * @function - * - * @description - * The controller for deleting content - */ -function YsodController($scope, legacyResource, treeService, navigationService) { - - if ($scope.error && $scope.error.data && $scope.error.data.StackTrace) { - //trim whitespace - $scope.error.data.StackTrace = $scope.error.data.StackTrace.trim(); - } - - $scope.closeDialog = function() { - $scope.dismiss(); - }; - -} - -angular.module("umbraco").controller("Umbraco.Dialogs.YsodController", YsodController); - -/** - * @ngdoc controller - * @name Umbraco.LegacyController - * @function - * - * @description - * A controller to control the legacy iframe injection - * -*/ -function LegacyController($scope, $routeParams, $element) { - - var url = decodeURIComponent($routeParams.url.replace(/javascript\:/gi, "")); - //split into path and query - var urlParts = url.split("?"); - var extIndex = urlParts[0].lastIndexOf("."); - var ext = extIndex === -1 ? "" : urlParts[0].substr(extIndex); - //path cannot be a js file - if (ext !== ".js" || ext === "") { - //path cannot contain any of these chars - var toClean = "*(){}[];:<>\\|'\""; - for (var i = 0; i < toClean.length; i++) { - var reg = new RegExp("\\" + toClean[i], "g"); - urlParts[0] = urlParts[0].replace(reg, ""); - } - //join cleaned path and query back together - url = urlParts[0] + (urlParts.length === 1 ? "" : ("?" + urlParts[1])); - $scope.legacyPath = url; - } - else { - throw "Invalid url"; - } -} - -angular.module("umbraco").controller('Umbraco.LegacyController', LegacyController); -/** This controller is simply here to launch the login dialog when the route is explicitly changed to /login */ -angular.module('umbraco').controller("Umbraco.LoginController", function (eventsService, $scope, userService, $location, $rootScope) { - - userService._showLoginDialog(); - - var evtOn = eventsService.on("app.ready", function(evt, data){ - $scope.avatar = "assets/img/application/logo.png"; - - var path = "/"; - - //check if there's a returnPath query string, if so redirect to it - var locationObj = $location.search(); - if (locationObj.returnPath) { - path = decodeURIComponent(locationObj.returnPath); - } - - $location.url(path); - }); - - $scope.$on('$destroy', function () { - eventsService.unsubscribe(evtOn); - }); - -}); - -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Notifications.ConfirmRouteChangeController", - function ($scope, $location, $log, notificationsService) { - - $scope.discard = function(not){ - not.args.listener(); - - $location.search(""); - - //we need to break the path up into path and query - var parts = not.args.path.split("?"); - var query = {}; - if (parts.length > 1) { - _.each(parts[1].split("&"), function(q) { - var keyVal = q.split("="); - query[keyVal[0]] = keyVal[1]; + $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; + $scope.showLogin = function () { + $scope.errorMsg = ''; + resetInputValidation(); + $scope.view = 'login'; + setFieldFocus('loginForm', 'username'); + }; + $scope.showRequestPasswordReset = function () { + $scope.errorMsg = ''; + resetInputValidation(); + $scope.view = 'request-password-reset'; + $scope.showEmailResetConfirmation = false; + setFieldFocus('requestPasswordResetForm', 'email'); + }; + $scope.showSetPassword = function () { + $scope.errorMsg = ''; + resetInputValidation(); + $scope.view = 'set-password'; + setFieldFocus('setPasswordForm', 'password'); + }; + var d = new Date(); + var konamiGreetings = new Array('Suze Sunday', 'Malibu Monday', 'Tequila Tuesday', 'Whiskey Wednesday', 'Negroni Day', 'Fernet Friday', 'Sancerre Saturday'); + var konamiMode = $cookies.konamiLogin; + if (konamiMode == '1') { + $scope.greeting = 'Happy ' + konamiGreetings[d.getDay()]; + } else { + localizationService.localize('login_greeting' + d.getDay()).then(function (label) { + $scope.greeting = label; + }); // weekday[d.getDay()]; + } + $scope.errorMsg = ''; + $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLoginInfo = externalLoginInfo; + $scope.resetPasswordCodeInfo = resetPasswordCodeInfo; + $scope.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; + $scope.activateKonamiMode = function () { + if ($cookies.konamiLogin == '1') { + // somehow I can't update the cookie value using $cookies, so going native + document.cookie = 'konamiLogin=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + document.location.reload(); + } else { + document.cookie = 'konamiLogin=1; expires=Tue, 01 Jan 2030 00:00:01 GMT;'; + $scope.$apply(function () { + $scope.greeting = 'Happy ' + konamiGreetings[d.getDay()]; }); } - - $location.path(parts[0]).search(query); - notificationsService.remove(not); - }; - - $scope.stay = function(not){ - notificationsService.remove(not); - }; - - }); - (function() { - "use strict"; - - function CompositionsOverlay($scope) { - - var vm = this; - - vm.isSelected = isSelected; - - function isSelected(alias) { - if($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { - return true; + }; + $scope.loginSubmit = function (login, password) { + //TODO: Do validation properly like in the invite password update + //if the login and password are not empty we need to automatically + // validate them - this is because if there are validation errors on the server + // then the user has to change both username & password to resubmit which isn't ideal, + // so if they're not empty, we'll just make sure to set them to valid. + if (login && password && login.length > 0 && password.length > 0) { + $scope.loginForm.username.$setValidity('auth', true); + $scope.loginForm.password.$setValidity('auth', true); } - } - } - - angular.module("umbraco").controller("Umbraco.Overlays.CompositionsOverlay", CompositionsOverlay); - -})(); - -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.PropertyController - * @function - * - * @description - * The controller for the content type editor property dialog - */ - -(function() { - "use strict"; - - function EditorPickerOverlay($scope, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService) { - - var vm = this; - - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectEditor"); - } - - vm.searchTerm = ""; - vm.showTabs = false; - vm.tabsLoaded = 0; - vm.typesAndEditors = []; - vm.userConfigured = []; - vm.loading = false; - vm.tabs = [{ - active: true, - id: 1, - label: localizationService.localize("contentTypeEditor_availableEditors"), - alias: "Default", - typesAndEditors: [] - }, { - active: false, - id: 2, - label: localizationService.localize("contentTypeEditor_reuse"), - alias: "Reuse", - userConfigured: [] - }]; - - vm.filterItems = filterItems; - vm.showDetailsOverlay = showDetailsOverlay; - vm.hideDetailsOverlay = hideDetailsOverlay; - vm.pickEditor = pickEditor; - vm.pickDataType = pickDataType; - - function activate() { - - getGroupedDataTypes(); - getGroupedPropertyEditors(); - - } - - function getGroupedPropertyEditors() { - - vm.loading = true; - - dataTypeResource.getGroupedPropertyEditors().then(function(data) { - vm.tabs[0].typesAndEditors = data; - vm.typesAndEditors = data; - vm.tabsLoaded = vm.tabsLoaded + 1; - checkIfTabContentIsLoaded(); + if ($scope.loginForm.$invalid) { + return; + } + userService.authenticate(login, password).then(function (data) { + $scope.submit(true); + }, function (reason) { + //is Two Factor required? + if (reason.status === 402) { + $scope.errorMsg = 'Additional authentication required'; + show2FALoginDialog(reason.data.twoFactorView, $scope.submit); + } else { + $scope.errorMsg = reason.errorMsg; + //set the form inputs to invalid + $scope.loginForm.username.$setValidity('auth', false); + $scope.loginForm.password.$setValidity('auth', false); + } }); - - } - - function getGroupedDataTypes() { - - vm.loading = true; - - dataTypeResource.getGroupedDataTypes().then(function(data) { - vm.tabs[1].userConfigured = data; - vm.userConfigured = data; - vm.tabsLoaded = vm.tabsLoaded + 1; - checkIfTabContentIsLoaded(); + //setup a watch for both of the model values changing, if they change + // while the form is invalid, then revalidate them so that the form can + // be submitted again. + $scope.loginForm.username.$viewChangeListeners.push(function () { + if ($scope.loginForm.username.$invalid) { + $scope.loginForm.username.$setValidity('auth', true); + } }); - - } - - function checkIfTabContentIsLoaded() { - if (vm.tabsLoaded === 2) { - vm.loading = false; - vm.showTabs = true; + $scope.loginForm.password.$viewChangeListeners.push(function () { + if ($scope.loginForm.password.$invalid) { + $scope.loginForm.password.$setValidity('auth', true); + } + }); + }; + $scope.requestPasswordResetSubmit = function (email) { + //TODO: Do validation properly like in the invite password update + if (email && email.length > 0) { + $scope.requestPasswordResetForm.email.$setValidity('auth', true); } - } - - function filterItems() { - // clear item details - $scope.model.itemDetails = null; - - if (vm.searchTerm) { - vm.showFilterResult = true; - vm.showTabs = false; - } else { - vm.showFilterResult = false; - vm.showTabs = true; - } - - } - - function showDetailsOverlay(property) { - - var propertyDetails = {}; - propertyDetails.icon = property.icon; - propertyDetails.title = property.name; - - $scope.model.itemDetails = propertyDetails; - - } - - function hideDetailsOverlay() { - $scope.model.itemDetails = null; - } - - function pickEditor(editor) { - - var parentId = -1; - - dataTypeResource.getScaffold(parentId).then(function(dataType) { - - // set alias - dataType.selectedEditor = editor.alias; - - // set name - var nameArray = []; - - if ($scope.model.contentTypeName) { - nameArray.push($scope.model.contentTypeName); - } - - if ($scope.model.property.label) { - nameArray.push($scope.model.property.label); - } - - if (editor.name) { - nameArray.push(editor.name); - } - - // make name - dataType.name = nameArray.join(" - "); - - // get pre values - dataTypeResource.getPreValues(dataType.selectedEditor).then(function(preValues) { - - dataType.preValues = preValues; - - openEditorSettingsOverlay(dataType, true); - - }); - + $scope.showEmailResetConfirmation = false; + if ($scope.requestPasswordResetForm.$invalid) { + return; + } + $scope.errorMsg = ''; + authResource.performRequestPasswordReset(email).then(function () { + //remove the email entered + $scope.email = ''; + $scope.showEmailResetConfirmation = true; + }, function (reason) { + $scope.errorMsg = reason.errorMsg; + $scope.requestPasswordResetForm.email.$setValidity('auth', false); }); - - } - - function pickDataType(selectedDataType) { - - dataTypeResource.getById(selectedDataType.id).then(function(dataType) { - contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function(propertyType) { - submitOverlay(dataType, propertyType, false); - }); + $scope.requestPasswordResetForm.email.$viewChangeListeners.push(function () { + if ($scope.requestPasswordResetForm.email.$invalid) { + $scope.requestPasswordResetForm.email.$setValidity('auth', true); + } }); - - } - - function openEditorSettingsOverlay(dataType, isNew) { - vm.editorSettingsOverlay = { - title: localizationService.localize("contentTypeEditor_editorSettings"), - dataType: dataType, - view: "views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html", - show: true, - submit: function(model) { - var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); - - dataTypeResource.save(model.dataType, preValues, isNew).then(function(newDataType) { - - contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function(propertyType) { - - submitOverlay(newDataType, propertyType, true); - - vm.editorSettingsOverlay.show = false; - vm.editorSettingsOverlay = null; - - }); - - }); + }; + $scope.setPasswordSubmit = function (password, confirmPassword) { + $scope.showSetPasswordConfirmation = false; + if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { + $scope.setPasswordForm.password.$setValidity('auth', true); + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + if ($scope.setPasswordForm.$invalid) { + return; + } + //TODO: All of this logic can/should be shared! We should do validation the nice way instead of all of this manual stuff, see: inviteSavePassword + authResource.performSetPassword($scope.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, $scope.resetPasswordCodeInfo.resetCodeModel.resetCode).then(function () { + $scope.showSetPasswordConfirmation = true; + $scope.resetComplete = true; + //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again + resetPasswordCodeInfo.resetCodeModel = null; + }, function (reason) { + if (reason.data && reason.data.Message) { + $scope.errorMsg = reason.data.Message; + } else { + $scope.errorMsg = reason.errorMsg; } - }; - - } - - function submitOverlay(dataType, propertyType, isNew) { - - // update property - $scope.model.property.config = propertyType.config; - $scope.model.property.editor = propertyType.editor; - $scope.model.property.view = propertyType.view; - $scope.model.property.dataTypeId = dataType.id; - $scope.model.property.dataTypeIcon = dataType.icon; - $scope.model.property.dataTypeName = dataType.name; - - $scope.model.updateSameDataTypes = isNew; - - $scope.model.submit($scope.model); - - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.Overlays.EditorPickerOverlay", EditorPickerOverlay); - -})(); - -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.PropertyController - * @function - * - * @description - * The controller for the content type editor property dialog - */ - - (function() { - "use strict"; - - function PropertySettingsOverlay($scope, contentTypeResource, dataTypeResource, dataTypeHelper, localizationService) { - - var vm = this; - - vm.showValidationPattern = false; - vm.focusOnPatternField = false; - vm.focusOnMandatoryField = false; - vm.selectedValidationType = {}; - vm.validationTypes = [ - { - "name": localizationService.localize("validation_validateAsEmail"), - "key": "email", - "pattern": "[a-zA-Z0-9_\.\+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-\.]+", - "enableEditing": true - }, - { - "name": localizationService.localize("validation_validateAsNumber"), - "key": "number", - "pattern": "^[0-9]*$", - "enableEditing": true - }, - { - "name": localizationService.localize("validation_validateAsUrl"), - "key": "url", - "pattern": "https?\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}", - "enableEditing": true - }, - { - "name": localizationService.localize("validation_enterCustomValidation"), - "key": "custom", - "pattern": "", - "enableEditing": true - } - ]; - - vm.changeValidationType = changeValidationType; - vm.changeValidationPattern = changeValidationPattern; - vm.openEditorPickerOverlay = openEditorPickerOverlay; - vm.openEditorSettingsOverlay = openEditorSettingsOverlay; - - function activate() { - - matchValidationType(); - - } - - function changeValidationPattern() { - matchValidationType(); - } - - function openEditorPickerOverlay(property) { - - vm.focusOnMandatoryField = false; - - vm.editorPickerOverlay = {}; - vm.editorPickerOverlay.property = $scope.model.property; - vm.editorPickerOverlay.contentTypeName = $scope.model.contentTypeName; - vm.editorPickerOverlay.view = "views/common/overlays/contenttypeeditor/editorpicker/editorpicker.html"; - vm.editorPickerOverlay.show = true; - - vm.editorPickerOverlay.submit = function(model) { - - $scope.model.updateSameDataTypes = model.updateSameDataTypes; - - vm.focusOnMandatoryField = true; - - // update property - property.config = model.property.config; - property.editor = model.property.editor; - property.view = model.property.view; - property.dataTypeId = model.property.dataTypeId; - property.dataTypeIcon = model.property.dataTypeIcon; - property.dataTypeName = model.property.dataTypeName; - - vm.editorPickerOverlay.show = false; - vm.editorPickerOverlay = null; - }; - - vm.editorPickerOverlay.close = function(model) { - vm.editorPickerOverlay.show = false; - vm.editorPickerOverlay = null; - }; - - } - - function openEditorSettingsOverlay(property) { - - vm.focusOnMandatoryField = false; - - // get data type - dataTypeResource.getById(property.dataTypeId).then(function(dataType) { - - vm.editorSettingsOverlay = {}; - vm.editorSettingsOverlay.title = "Editor settings"; - vm.editorSettingsOverlay.view = "views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html"; - vm.editorSettingsOverlay.dataType = dataType; - vm.editorSettingsOverlay.show = true; - - vm.editorSettingsOverlay.submit = function(model) { - - var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); - - dataTypeResource.save(model.dataType, preValues, false).then(function(newDataType) { - - contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function(propertyType) { - - // update editor - property.config = propertyType.config; - property.editor = propertyType.editor; - property.view = propertyType.view; - property.dataTypeId = newDataType.id; - property.dataTypeIcon = newDataType.icon; - property.dataTypeName = newDataType.name; - - // set flag to update same data types - $scope.model.updateSameDataTypes = true; - - vm.focusOnMandatoryField = true; - - vm.editorSettingsOverlay.show = false; - vm.editorSettingsOverlay = null; - - }); - - }); - - }; - - vm.editorSettingsOverlay.close = function(oldModel) { - vm.editorSettingsOverlay.show = false; - vm.editorSettingsOverlay = null; - }; - - }); - - } - - function matchValidationType() { - - if($scope.model.property.validation.pattern !== null && $scope.model.property.validation.pattern !== "" && $scope.model.property.validation.pattern !== undefined) { - - var match = false; - - // find and show if a match from the list has been chosen - angular.forEach(vm.validationTypes, function(validationType, index){ - if($scope.model.property.validation.pattern === validationType.pattern) { - vm.selectedValidationType = vm.validationTypes[index]; - vm.showValidationPattern = true; - match = true; - } + $scope.setPasswordForm.password.$setValidity('auth', false); + $scope.setPasswordForm.confirmPassword.$setValidity('auth', false); }); - - // if there is no match - choose the custom validation option. - if(!match) { - angular.forEach(vm.validationTypes, function(validationType){ - if(validationType.key === "custom") { - vm.selectedValidationType = validationType; - vm.showValidationPattern = true; - } - }); - } - } - - } - - function changeValidationType(selectedValidationType) { - - if(selectedValidationType) { - $scope.model.property.validation.pattern = selectedValidationType.pattern; - vm.showValidationPattern = true; - - // set focus on textarea - if(selectedValidationType.key === "custom") { - vm.focusOnPatternField = true; - } - - } else { - $scope.model.property.validation.pattern = ""; - vm.showValidationPattern = false; - } - - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.Overlay.PropertySettingsOverlay", PropertySettingsOverlay); - -})(); - - (function() { - "use strict"; - - function CopyOverlay($scope, localizationService, eventsService, entityHelper) { - - var vm = this; - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("general_copy"); - } - - vm.hideSearch = hideSearch; - vm.selectResult = selectResult; - vm.onSearchResults = onSearchResults; - - var dialogOptions = $scope.model; - var searchText = "Search..."; - var node = dialogOptions.currentNode; - - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - $scope.model.relateToOriginal = true; - $scope.dialogTreeEventHandler = $({}); - - vm.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; - - // get entity type based on the section - $scope.entityType = entityHelper.getEntityTypeFromSection(dialogOptions.section); - - function nodeSelectHandler(ev, args) { - if(args && args.event) { - args.event.preventDefault(); - args.event.stopPropagation(); - } - - //eventsService.emit("editors.content.copyController.select", args); - - if ($scope.model.target) { - //un-select if there's a current one selected - $scope.model.target.selected = false; - } - - $scope.model.target = args.node; - $scope.model.target.selected = true; - } - - function nodeExpandedHandler(ev, args) { - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - } - - function hideSearch() { - vm.searchInfo.showSearch = false; - vm.searchInfo.searchFromId = null; - vm.searchInfo.searchFromName = null; - vm.searchInfo.results = []; - } - - // method to select a search result - function selectResult(evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { event: evt, node: result }); - } - - //callback when there are search results - function onSearchResults(results) { - vm.searchInfo.results = results; - vm.searchInfo.showSearch = true; - } - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - - // Mini list view - $scope.selectListViewNode = function (node) { - node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - - } - - angular.module("umbraco").controller("Umbraco.Overlays.CopyOverlay", CopyOverlay); - -})(); - -(function() { - "use strict"; - - function EmbedOverlay($scope, $http, umbRequestHelper, localizationService) { - - var vm = this; - var origWidth = 500; - var origHeight = 300; - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("general_embed"); - } - - $scope.model.embed = { - url: "", - width: 360, - height: 240, - constrain: true, - preview: "", - success: false, - info: "", - supportsDimensions: "" - }; - - vm.showPreview = showPreview; - vm.changeSize = changeSize; - - function showPreview() { - - if ($scope.model.embed.url) { - $scope.model.embed.show = true; - $scope.model.embed.preview = "
"; - $scope.model.embed.info = ""; - $scope.model.embed.success = false; - - $http({ - method: 'GET', - url: umbRequestHelper.getApiUrl("embedApiBaseUrl", "GetEmbed"), - params: { - url: $scope.model.embed.url, - width: $scope.model.embed.width, - height: $scope.model.embed.height - } - }) - .success(function(data) { - - $scope.model.embed.preview = ""; - - switch (data.Status) { - case 0: - //not supported - $scope.model.embed.info = "Not supported"; - break; - case 1: - //error - $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; - break; - case 2: - $scope.model.embed.preview = data.Markup; - $scope.model.embed.supportsDimensions = data.SupportsDimensions; - $scope.model.embed.success = true; - break; - } - }) - .error(function() { - $scope.model.embed.supportsDimensions = false; - $scope.model.embed.preview = ""; - $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; - }); - } else { - $scope.model.embed.supportsDimensions = false; - $scope.model.embed.preview = ""; - $scope.model.embed.info = "Please enter a URL"; - } - } - - function changeSize(type) { - - var width, height; - - if ($scope.model.embed.constrain) { - width = parseInt($scope.model.embed.width, 10); - height = parseInt($scope.model.embed.height, 10); - if (type == 'width') { - origHeight = Math.round((width / origWidth) * height); - $scope.model.embed.height = origHeight; - } else { - origWidth = Math.round((height / origHeight) * width); - $scope.model.embed.width = origWidth; - } - } - if ($scope.model.embed.url !== "") { - showPreview(); - } - - } - - } - - angular.module("umbraco").controller("Umbraco.Overlays.EmbedOverlay", EmbedOverlay); - -})(); - -angular.module("umbraco") - .controller("Umbraco.Overlays.HelpController", function ($scope, $location, $routeParams, helpService, userService, localizationService) { - $scope.section = $routeParams.section; - $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - $scope.model.subtitle = "Umbraco version" + " " + $scope.version; - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("general_help"); - } - - if(!$scope.section){ - $scope.section = "content"; - } - - $scope.sectionName = $scope.section; - - var rq = {}; - rq.section = $scope.section; - - //translate section name - localizationService.localize("sections_" + rq.section).then(function (value) { - $scope.sectionName = value; - }); - - userService.getCurrentUser().then(function(user){ - - rq.usertype = user.userType; - rq.lang = user.locale; - - if($routeParams.url){ - rq.path = decodeURIComponent($routeParams.url); - - if(rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0){ - rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); - } - - if(rq.path.indexOf(".aspx") > 0){ - rq.path = rq.path.substring(0, rq.path.indexOf(".aspx")); - } - - }else{ - rq.path = rq.section + "/" + $routeParams.tree + "/" + $routeParams.method; - } - - helpService.findHelp(rq).then(function(topics){ - $scope.topics = topics; - }); - - helpService.findVideos(rq).then(function(videos){ - $scope.videos = videos; - }); - - }); - - - }); - -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.PropertyController - * @function - * - * @description - * The controller for the content type editor property dialog - */ -function IconPickerOverlay($scope, iconHelper, localizationService) { - - $scope.loading = true; - $scope.model.hideSubmitButton = true; - - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectIcon"); - } - - iconHelper.getIcons().then(function(icons) { - $scope.icons = icons; - $scope.loading = false; - }); - - $scope.selectIcon = function(icon, color) { - $scope.model.icon = icon; - $scope.model.color = color; - $scope.submitForm($scope.model); - }; - -} - -angular.module("umbraco").controller("Umbraco.Overlays.IconPickerOverlay", IconPickerOverlay); - -(function () { - "use strict"; - - function InsertOverlayController($scope, localizationService) { - - var vm = this; - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("template_insert"); - } - - if(!$scope.model.subtitle) { - $scope.model.subtitle = localizationService.localize("template_insertDesc"); - } - - vm.openMacroPicker = openMacroPicker; - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openPartialOverlay = openPartialOverlay; - - function openMacroPicker() { - - vm.macroPickerOverlay = { - view: "macropicker", - title: localizationService.localize("template_insertMacro"), - dialogData: {}, - show: true, - submit: function(model) { - - $scope.model.insert = { - "type": "macro", - "macroParams": model.macroParams, - "selectedMacro": model.selectedMacro - }; - - $scope.model.submit($scope.model); - - vm.macroPickerOverlay.show = false; - vm.macroPickerOverlay = null; - + $scope.setPasswordForm.password.$viewChangeListeners.push(function () { + if ($scope.setPasswordForm.password.$invalid) { + $scope.setPasswordForm.password.$setValidity('auth', true); } - }; - - } - - function openPageFieldOverlay() { - vm.pageFieldOverlay = { - title: localizationService.localize("template_insertPageField"), - description: localizationService.localize("template_insertPageFieldDesc"), - submitButtonLabel: "Insert", - closeButtonlabel: "Cancel", - view: "insertfield", - show: true, - submit: function(model) { - - $scope.model.insert = { - "type": "umbracoField", - "umbracoField": model.umbracoField - }; - - $scope.model.submit($scope.model); - - vm.pageFieldOverlay.show = false; - vm.pageFieldOverlay = null; - }, - close: function (model) { - vm.pageFieldOverlay.show = false; - vm.pageFieldOverlay = null; + }); + $scope.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { + if ($scope.setPasswordForm.confirmPassword.$invalid) { + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); } - }; + }); + }; + //Now, show the correct panel: + if ($scope.resetPasswordCodeInfo.resetCodeModel) { + $scope.showSetPassword(); + } else if ($scope.resetPasswordCodeInfo.errors.length > 0) { + $scope.view = 'password-reset-code-expired'; + } else { + $scope.showLogin(); } - - function openDictionaryItemOverlay() { - - vm.dictionaryItemOverlay = { - view: "treepicker", - section: "settings", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - title: localizationService.localize("template_insertDictionaryItem"), - description: localizationService.localize("template_insertDictionaryItemDesc"), - emptyStateMessage: localizationService.localize("emptyStates_emptyDictionaryTree"), - show: true, - select: function(node){ - - $scope.model.insert = { - "type": "dictionary", - "node": node - }; - - $scope.model.submit($scope.model); - - vm.dictionaryItemOverlay.show = false; - vm.dictionaryItemOverlay = null; - }, - - close: function(model) { - vm.dictionaryItemOverlay.show = false; - vm.dictionaryItemOverlay = null; - } - }; - } - - function openPartialOverlay() { - vm.partialItemOverlay = { - view: "treepicker", - section: "settings", - treeAlias: "partialViews", - entityType: "partialView", - multiPicker: false, - filter: function(i) { - if(i.name.indexOf(".cshtml") === -1 && i.name.indexOf(".vbhtml") === -1) { - return true; - } - }, - filterCssClass: "not-allowed", - title: localizationService.localize("template_insertPartialView"), - show: true, - select: function(node){ - - $scope.model.insert = { - "type": "partial", - "node": node - }; - - $scope.model.submit($scope.model); - - vm.partialItemOverlay.show = false; - vm.partialItemOverlay = null; - }, - - close: function (model) { - vm.partialItemOverlay.show = false; - vm.partialItemOverlay = null; - } - }; - } - - } - - angular.module("umbraco").controller("Umbraco.Overlays.InsertOverlay", InsertOverlayController); -})(); - -(function () { - "use strict"; - - function InsertFieldController($scope, $http, contentTypeResource) { - - var vm = this; - - vm.field; - vm.altField; - vm.altText; - vm.insertBefore; - vm.insertAfter; - vm.recursive = false; - vm.properties = []; - vm.standardFields = []; - vm.date = false; - vm.dateTime = false; - vm.dateTimeSeparator = ""; - vm.casingUpper = false; - vm.casingLower = false; - vm.encodeHtml = false; - vm.encodeUrl = false; - vm.convertLinebreaks = false; - vm.removeParagraphTags = false; - - vm.showAltField = false; - vm.showAltText = false; - - vm.setDateOption = setDateOption; - vm.setCasingOption = setCasingOption; - vm.setEncodingOption = setEncodingOption; - vm.generateOutputSample = generateOutputSample; - - function onInit() { - - // set default title - if(!$scope.model.title) { - $scope.model.title = "Insert value"; - } - - // Load all fields - contentTypeResource.getAllPropertyTypeAliases().then(function (array) { - vm.properties = array; - }); - - // Load all standard fields - contentTypeResource.getAllStandardFields().then(function (array) { - vm.standardFields = array; - }); - - } - - // date formatting - function setDateOption(option) { - - if (option === 'date') { - if(vm.date) { - vm.date = false; - } else { - vm.date = true; - vm.dateTime = false; - } + init(); + }); + //used for the macro picker dialog + angular.module('umbraco').controller('Umbraco.Dialogs.MacroPickerController', function ($scope, macroFactory, umbPropEditorHelper) { + $scope.macros = macroFactory.all(true); + $scope.dialogMode = 'list'; + $scope.configureMacro = function (macro) { + $scope.dialogMode = 'configure'; + $scope.dialogData.macro = macroFactory.getMacro(macro.alias); + //set the correct view for each item + for (var i = 0; i < dialogData.macro.properties.length; i++) { + dialogData.macro.properties[i].editorView = umbPropEditorHelper.getViewPath(dialogData.macro.properties[i].view); } - - if (option === 'dateWithTime') { - if(vm.dateTime) { - vm.dateTime = false; - } else { - vm.date = false; - vm.dateTime = true; - } + }; + }); + //used for the media picker dialog + angular.module('umbraco').controller('Umbraco.Dialogs.MediaPickerController', function ($scope, mediaResource, umbRequestHelper, entityResource, $log, mediaHelper, mediaTypeHelper, eventsService, treeService) { + var dialogOptions = $scope.dialogOptions; + $scope.onlyImages = dialogOptions.onlyImages; + $scope.showDetails = dialogOptions.showDetails; + $scope.multiPicker = dialogOptions.multiPicker && dialogOptions.multiPicker !== '0' ? true : false; + $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; + $scope.cropSize = dialogOptions.cropSize; + //preload selected item + $scope.target = undefined; + if (dialogOptions.currentTarget) { + $scope.target = dialogOptions.currentTarget; + } + $scope.acceptedMediatypes = []; + mediaTypeHelper.getAllowedImagetypes($scope.startNodeId).then(function (types) { + $scope.acceptedMediatypes = types; + }); + $scope.upload = function (v) { + angular.element('.umb-file-dropzone-directive .file-select').click(); + }; + $scope.dragLeave = function (el, event) { + $scope.activeDrag = false; + }; + $scope.dragEnter = function (el, event) { + $scope.activeDrag = true; + }; + $scope.submitFolder = function (e) { + if (e.keyCode === 13) { + e.preventDefault(); + mediaResource.addFolder($scope.newFolderName, $scope.currentFolder.id).then(function (data) { + $scope.showFolderInput = false; + $scope.newFolderName = ''; + //we've added a new folder so lets clear the tree cache for that specific item + treeService.clearCache({ + cacheKey: '__media', + //this is the main media tree cache key + childrenOf: data.parentId + }); + $scope.gotoFolder(data); + }); } - - } - - // casing formatting - function setCasingOption(option) { - if (option === 'uppercase') { - if(vm.casingUpper) { - vm.casingUpper = false; - } else { - vm.casingUpper = true; - vm.casingLower = false; - } + }; + $scope.gotoFolder = function (folder) { + if (!folder) { + folder = { + id: -1, + name: 'Media', + icon: 'icon-folder' + }; + } + if (folder.id > 0) { + entityResource.getAncestors(folder.id, 'media').then(function (anc) { + // anc.splice(0,1); + $scope.path = _.filter(anc, function (f) { + return f.path.indexOf($scope.startNodeId) !== -1; + }); + }); + mediaTypeHelper.getAllowedImagetypes(folder.id).then(function (types) { + $scope.acceptedMediatypes = types; + }); + } else { + $scope.path = []; } - - if (option === 'lowercase') { - if(vm.casingLower) { - vm.casingLower = false; + //mediaResource.rootMedia() + mediaResource.getChildren(folder.id).then(function (data) { + $scope.searchTerm = ''; + $scope.images = data.items ? data.items : []; + }); + $scope.currentFolder = folder; + }; + $scope.clickHandler = function (image, ev, select) { + ev.preventDefault(); + if (image.isFolder && !select) { + $scope.gotoFolder(image); + } else { + eventsService.emit('dialogs.mediaPicker.select', image); + //we have 3 options add to collection (if multi) show details, or submit it right back to the callback + if ($scope.multiPicker) { + $scope.select(image); + image.cssclass = $scope.dialogData.selection.indexOf(image) > -1 ? 'selected' : ''; + } else if ($scope.showDetails) { + $scope.target = image; + $scope.target.url = mediaHelper.resolveFile(image); } else { - vm.casingUpper = false; - vm.casingLower = true; + $scope.submit(image); } } + }; + $scope.exitDetails = function () { + if (!$scope.currentFolder) { + $scope.gotoFolder(); + } + $scope.target = undefined; + }; + $scope.onUploadComplete = function () { + $scope.gotoFolder($scope.currentFolder); + }; + $scope.onFilesQueue = function () { + $scope.activeDrag = false; + }; + //default root item + if (!$scope.target) { + $scope.gotoFolder({ + id: $scope.startNodeId, + name: 'Media', + icon: 'icon-folder' + }); } - - // encoding formatting - function setEncodingOption(option) { - if (option === 'html') { - if(vm.encodeHtml) { - vm.encodeHtml = false; - } else { - vm.encodeHtml = true; - vm.encodeUrl = false; - } + }); + //used for the member picker dialog + angular.module('umbraco').controller('Umbraco.Dialogs.MemberGroupPickerController', function ($scope, eventsService, entityResource, searchService, $log) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + $scope.multiPicker = dialogOptions.multiPicker; + /** Method used for selecting a node */ + function select(text, id) { + if (dialogOptions.multiPicker) { + $scope.select(id); + } else { + $scope.submit(id); } - - if (option === 'url') { - if (vm.encodeUrl) { - vm.encodeUrl = false; - } else { - vm.encodeHtml = false; - vm.encodeUrl = true; - } - } - } - - function generateOutputSample() { - - var pageField = (vm.field !== undefined ? '@Umbraco.Field("' + vm.field + '"' : "") - + (vm.altField !== undefined ? ', altFieldAlias:"' + vm.altField + '"' : "") - + (vm.altText !== undefined ? ', altText:"' + vm.altText + '"' : "") - + (vm.insertBefore !== undefined ? ', insertBefore:"' + vm.insertBefore + '"' : "") - + (vm.insertAfter !== undefined ? ', insertAfter:"' + vm.insertAfter + '"' : "") - + (vm.recursive !== false ? ', recursive: ' + vm.recursive : "") - + (vm.date !== false ? ', formatAsDate: ' + vm.date : "") - + (vm.dateTime !== false ? ', formatAsDateWithTimeSeparator:"' + vm.dateTimeSeparator + '"' : "") - + (vm.casingUpper !== false ? ', casing: ' + "RenderFieldCaseType.Upper" : "") - + (vm.casingLower !== false ? ', casing: ' + "RenderFieldCaseType.Lower" : "") - + (vm.encodeHtml !== false ? ', encoding: ' + "RenderFieldEncodingType.Html" : "") - + (vm.encodeUrl !== false ? ', encoding: ' + "RenderFieldEncodingType.Url" : "") - + (vm.convertLinebreaks !== false ? ', convertLineBreaks: ' + "true" : "") - + (vm.removeParagraphTags !== false ? ', removeParagraphTags: ' + "true": "") - + (vm.field ? ')' : ""); - - $scope.model.umbracoField = pageField; - - return pageField; - - } - - onInit(); - - - } - - angular.module("umbraco").controller("Umbraco.Overlays.InsertFieldController", InsertFieldController); -})(); - -function ItemPickerOverlay($scope, localizationService) { - - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectItem"); - } - - $scope.model.hideSubmitButton = true; - - $scope.selectItem = function(item) { - $scope.model.selectedItem = item; - $scope.submitForm($scope.model); - }; - -} - -angular.module("umbraco").controller("Umbraco.Overlays.ItemPickerOverlay", ItemPickerOverlay); - -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { - var dialogOptions = $scope.model; - - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); - } - - $scope.dialogTreeEventHandler = $({}); - $scope.model.target = {}; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; - - if (dialogOptions.currentTarget) { - $scope.model.target = dialogOptions.currentTarget; - - //if we have a node ID, we fetch the current node to build the form data - if ($scope.model.target.id || $scope.model.target.udi) { - - //will be either a udi or an int - var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; - - if (!$scope.model.target.path) { - - entityResource.getPath(id, "Document").then(function (path) { - $scope.model.target.path = path; - //now sync the tree to this path - $scope.dialogTreeEventHandler.syncTree({ path: $scope.model.target.path, tree: "content" }); - }); - } - - contentResource.getNiceUrl(id).then(function (url) { - $scope.model.target.url = url; - }); - } - } - - function nodeSelectHandler(ev, args) { - - if(args && args.event) { - args.event.preventDefault(); - args.event.stopPropagation(); - } - - eventsService.emit("dialogs.linkPicker.select", args); - - if ($scope.currentNode) { - //un-select if there's a current one selected - $scope.currentNode.selected = false; - } - - $scope.currentNode = args.node; - $scope.currentNode.selected = true; - $scope.model.target.id = args.node.id; - $scope.model.target.udi = args.node.udi; - $scope.model.target.name = args.node.name; - - if (args.node.id < 0) { - $scope.model.target.url = "/"; - } - else { - contentResource.getNiceUrl(args.node.id).then(function (url) { - $scope.model.target.url = url; - }); - } - - if (!angular.isUndefined($scope.model.target.isMedia)) { - delete $scope.model.target.isMedia; - } - } - - function nodeExpandedHandler(ev, args) { - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - } - - $scope.switchToMediaPicker = function () { - userService.getCurrentUser().then(function (userData) { - $scope.mediaPickerOverlay = { - view: "mediapicker", - startNodeId: userData.startMediaId, - show: true, - submit: function(model) { - var media = model.selectedImages[0]; - - $scope.model.target.id = media.id; - $scope.model.target.udi = media.udi; - $scope.model.target.isMedia = true; - $scope.model.target.name = media.name; - $scope.model.target.url = mediaHelper.resolveFile(media); - - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - } - }; - }); - }; - - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, {event: evt, node: result}); - }; - - //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; - $scope.searchInfo.showSearch = true; - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - - // Mini list view - $scope.selectListViewNode = function (node) { - node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - }); - -function MacroPickerController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper, localizationService) { - - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMacro"); - } - - $scope.macros = []; - $scope.model.selectedMacro = null; - $scope.model.macroParams = []; - - $scope.wizardStep = "macroSelect"; - $scope.noMacroParams = false; - - $scope.selectMacro = function (macro) { - - $scope.model.selectedMacro = macro; - - if ($scope.wizardStep === "macroSelect") { - editParams(); - } else { - $scope.model.submit($scope.model); } - }; - - /** changes the view to edit the params of the selected macro */ - function editParams() { - //get the macro params if there are any - macroResource.getMacroParameters($scope.model.selectedMacro.id) - .then(function (data) { - - - - //go to next page if there are params otherwise we can just exit - if (!angular.isArray(data) || data.length === 0) { - - $scope.model.submit($scope.model); - - } else { - - $scope.wizardStep = "paramSelect"; - $scope.model.macroParams = data; - - //fill in the data if we are editing this macro - if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroParamsDictionary) { - _.each($scope.model.dialogData.macroData.macroParamsDictionary, function (val, key) { - var prop = _.find($scope.model.macroParams, function (item) { - return item.alias == key; - }); - if (prop) { - - if (_.isString(val)) { - //we need to unescape values as they have most likely been escaped while inserted - val = _.unescape(val); - - //detect if it is a json string - if (val.detectIsJson()) { - try { - //Parse it to json - prop.value = angular.fromJson(val); - } - catch (e) { - // not json - prop.value = val; - } - } - else { - prop.value = val; - } - } - else { - prop.value = val; - } - } - }); - + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + eventsService.emit('dialogs.memberGroupPicker.select', args); + //This is a tree node, so we don't have an entity to pass in, it will need to be looked up + //from the server in this method. + select(args.node.name, args.node.id); + //toggle checked state + args.node.selected = args.node.selected === true ? false : true; + } + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + }); + }); + angular.module('umbraco').controller('Umbraco.Dialogs.RteEmbedController', function ($scope, $http, umbRequestHelper) { + $scope.form = {}; + $scope.form.url = ''; + $scope.form.width = 360; + $scope.form.height = 240; + $scope.form.constrain = true; + $scope.form.preview = ''; + $scope.form.success = false; + $scope.form.info = ''; + $scope.form.supportsDimensions = false; + var origWidth = 500; + var origHeight = 300; + $scope.showPreview = function () { + if ($scope.form.url) { + $scope.form.show = true; + $scope.form.preview = '
'; + $scope.form.info = ''; + $scope.form.success = false; + $http({ + method: 'GET', + url: umbRequestHelper.getApiUrl('embedApiBaseUrl', 'GetEmbed'), + params: { + url: $scope.form.url, + width: $scope.form.width, + height: $scope.form.height } - } - - }); - } - - //here we check to see if we've been passed a selected macro and if so we'll set the - //editor to start with parameter editing - if ($scope.model.dialogData && $scope.model.dialogData.macroData) { - $scope.wizardStep = "paramSelect"; - } - - //get the macro list - pass in a filter if it is only for rte - entityResource.getAll("Macro", ($scope.model.dialogData && $scope.model.dialogData.richTextEditor && $scope.model.dialogData.richTextEditor === true) ? "UseInEditor=true" : null) - .then(function (data) { - - if (angular.isArray(data) && data.length == 0) { - $scope.nomacros = true; - } - - //if 'allowedMacros' is specified, we need to filter - if (angular.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { - $scope.macros = _.filter(data, function(d) { - return _.contains($scope.model.dialogData.allowedMacros, d.alias); + }).success(function (data) { + $scope.form.preview = ''; + switch (data.Status) { + case 0: + //not supported + $scope.form.info = 'Not supported'; + break; + case 1: + //error + $scope.form.info = 'Could not embed media - please ensure the URL is valid'; + break; + case 2: + $scope.form.preview = data.Markup; + $scope.form.supportsDimensions = data.SupportsDimensions; + $scope.form.success = true; + break; + } + }).error(function () { + $scope.form.supportsDimensions = false; + $scope.form.preview = ''; + $scope.form.info = 'Could not embed media - please ensure the URL is valid'; }); + } else { + $scope.form.supportsDimensions = false; + $scope.form.preview = ''; + $scope.form.info = 'Please enter a URL'; } - else { - $scope.macros = data; - } - - - //check if there's a pre-selected macro and if it exists - if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroAlias) { - var found = _.find(data, function (item) { - return item.alias === $scope.model.dialogData.macroData.macroAlias; - }); - if (found) { - //select the macro and go to next screen - $scope.model.selectedMacro = found; - editParams(); - return; + }; + $scope.changeSize = function (type) { + var width, height; + if ($scope.form.constrain) { + width = parseInt($scope.form.width, 10); + height = parseInt($scope.form.height, 10); + if (type == 'width') { + origHeight = Math.round(width / origWidth * height); + $scope.form.height = origHeight; + } else { + origWidth = Math.round(height / origHeight * width); + $scope.form.width = origWidth; } } - //we don't have a pre-selected macro so ensure the correct step is set - $scope.wizardStep = "macroSelect"; + if ($scope.form.url != '') { + $scope.showPreview(); + } + }; + $scope.insert = function () { + $scope.submit($scope.form.preview); + }; + }); + angular.module('umbraco').controller('Umbraco.Dialogs.Template.QueryBuilderController', function ($scope, $http, dialogService) { + $http.get('backoffice/UmbracoApi/TemplateQuery/GetAllowedProperties').then(function (response) { + $scope.properties = response.data; }); - - -} - -angular.module("umbraco").controller("Umbraco.Overlays.MacroPickerController", MacroPickerController); - -//used for the media picker dialog -angular.module("umbraco") - .controller("Umbraco.Overlays.MediaPickerController", - function ($scope, mediaResource, umbRequestHelper, entityResource, $log, mediaHelper, mediaTypeHelper, eventsService, treeService, $element, $timeout, $cookies, localStorageService, localizationService) { - - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); - } - - var dialogOptions = $scope.model; - - $scope.disableFolderSelect = dialogOptions.disableFolderSelect; - $scope.onlyImages = dialogOptions.onlyImages; - $scope.showDetails = dialogOptions.showDetails; - $scope.multiPicker = (dialogOptions.multiPicker && dialogOptions.multiPicker !== "0") ? true : false; - $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; - $scope.cropSize = dialogOptions.cropSize; - $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); - - var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; - var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); - if ($scope.onlyImages) { - $scope.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); - } else { - // Use whitelist of allowed file types if provided - if (allowedUploadFiles !== '') { - $scope.acceptedFileTypes = allowedUploadFiles; - } else { - // If no whitelist, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles - $scope.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); - } - } - - $scope.maxFileSize = umbracoSettings.maxFileSize + "KB"; - - $scope.model.selectedImages = []; - - $scope.acceptedMediatypes = []; - mediaTypeHelper.getAllowedImagetypes($scope.startNodeId) - .then(function(types) { - $scope.acceptedMediatypes = types; - }); - - $scope.searchOptions = { - pageNumber: 1, - pageSize: 100, - totalItems: 0, - totalPages: 0, - filter: '', - }; - - //preload selected item - $scope.target = undefined; - if (dialogOptions.currentTarget) { - $scope.target = dialogOptions.currentTarget; - } - - $scope.upload = function(v) { - angular.element(".umb-file-dropzone-directive .file-select").click(); - }; - - $scope.dragLeave = function(el, event) { - $scope.activeDrag = false; - }; - - $scope.dragEnter = function(el, event) { - $scope.activeDrag = true; - }; - - $scope.submitFolder = function() { - if ($scope.newFolderName) { - $scope.creatingFolder = true; - mediaResource - .addFolder($scope.newFolderName, $scope.currentFolder.id) - .then(function(data) { - //we've added a new folder so lets clear the tree cache for that specific item - treeService.clearCache({ - cacheKey: "__media", //this is the main media tree cache key - childrenOf: data.parentId //clear the children of the parent - }); - $scope.creatingFolder = false; - $scope.gotoFolder(data); - $scope.showFolderInput = false; - $scope.newFolderName = ""; - }); - } else { - $scope.showFolderInput = false; - } - }; - - $scope.enterSubmitFolder = function(event) { - if (event.keyCode === 13) { - $scope.submitFolder(); - event.stopPropagation(); - } - }; - - $scope.gotoFolder = function(folder) { - - if (!$scope.multiPicker) { - deselectAllImages($scope.model.selectedImages); - } - - if (!folder) { - folder = { id: -1, name: "Media", icon: "icon-folder" }; - } - - if (folder.id > 0) { - entityResource.getAncestors(folder.id, "media") - .then(function(anc) { - // anc.splice(0,1); - $scope.path = _.filter(anc, - function(f) { - return f.path.indexOf($scope.startNodeId) !== -1; - }); - }); - - mediaTypeHelper.getAllowedImagetypes(folder.id) - .then(function(types) { - $scope.acceptedMediatypes = types; - }); - } else { - $scope.path = []; - } - - getChildren(folder.id); - $scope.currentFolder = folder; - localStorageService.set("umbLastOpenedMediaNodeId", folder.id); - }; - - $scope.clickHandler = function (image, event, index) { - if (image.isFolder) { - if ($scope.disableFolderSelect) { - $scope.gotoFolder(image); - } else { - eventsService.emit("dialogs.mediaPicker.select", image); - selectImage(image); - } - } else { - eventsService.emit("dialogs.mediaPicker.select", image); - if ($scope.showDetails) { - - $scope.target = image; - - // handle both entity and full media object - if(image.image) { - $scope.target.url = image.image; - } else { - $scope.target.url = mediaHelper.resolveFile(image); - } - - $scope.openDetailsDialog(); - } else { - selectImage(image); - } - } - }; - - $scope.clickItemName = function(item) { - if (item.isFolder) { - $scope.gotoFolder(item); - } - }; - - function selectImage(image) { - if (image.selected) { - for (var i = 0; $scope.model.selectedImages.length > i; i++) { - var imageInSelection = $scope.model.selectedImages[i]; - if (image.key === imageInSelection.key) { - image.selected = false; - $scope.model.selectedImages.splice(i, 1); - } - } - } else { - if (!$scope.multiPicker) { - deselectAllImages($scope.model.selectedImages); - } - image.selected = true; - $scope.model.selectedImages.push(image); - } - } - - function deselectAllImages(images) { - for (var i = 0; i < images.length; i++) { - var image = images[i]; - image.selected = false; - } - images.length = 0; - } - - $scope.onUploadComplete = function() { - $scope.gotoFolder($scope.currentFolder); - }; - - $scope.onFilesQueue = function() { - $scope.activeDrag = false; - }; - - function ensureWithinStartNode(node) { - // make sure that last opened node is on the same path as start node - var nodePath = node.path.split(","); - - if (nodePath.indexOf($scope.startNodeId.toString()) !== -1) { - $scope.gotoFolder({ id: $scope.lastOpenedNode, name: "Media", icon: "icon-folder" }); - return true; - } - else { - $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); - return false; - } - } - - function gotoStartNode(err) { - $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); - } - - //default root item - if (!$scope.target) { - if ($scope.lastOpenedNode && $scope.lastOpenedNode !== -1) { - entityResource.getById($scope.lastOpenedNode, "media") - .then(ensureWithinStartNode, gotoStartNode); - } - else { - gotoStartNode(); - } - } - else { - //if a target is specified, go look it up - generally this target will just contain ids not the actual full - //media object so we need to look it up - var id = $scope.target.udi ? $scope.target.udi : $scope.target.id - var altText = $scope.target.altText; - mediaResource.getById(id) - .then(function (node) { - $scope.target = node; - if (ensureWithinStartNode(node)) { - selectImage(node); - $scope.target.url = mediaHelper.resolveFile(node); - $scope.target.altText = altText; - $scope.openDetailsDialog(); - } - }, gotoStartNode); - } - - $scope.openDetailsDialog = function() { - - $scope.mediaPickerDetailsOverlay = {}; - $scope.mediaPickerDetailsOverlay.show = true; - - $scope.mediaPickerDetailsOverlay.submit = function(model) { - $scope.model.selectedImages.push($scope.target); - $scope.model.submit($scope.model); - - $scope.mediaPickerDetailsOverlay.show = false; - $scope.mediaPickerDetailsOverlay = null; - }; - - $scope.mediaPickerDetailsOverlay.close = function(oldModel) { - $scope.mediaPickerDetailsOverlay.show = false; - $scope.mediaPickerDetailsOverlay = null; - }; - }; - - $scope.changeSearch = function() { - $scope.loading = true; - debounceSearchMedia(); - }; - - $scope.changePagination = function(pageNumber) { - $scope.loading = true; - $scope.searchOptions.pageNumber = pageNumber; - searchMedia(); - }; - - var debounceSearchMedia = _.debounce(function () { - $scope.$apply(function () { - if ($scope.searchOptions.filter) { - searchMedia(); - } else { - // reset pagination - $scope.searchOptions = { - pageNumber: 1, - pageSize: 100, - totalItems: 0, - totalPages: 0, - filter: '' - }; - getChildren($scope.currentFolder.id); - } - }); - }, 500); - - function searchMedia() { - $scope.loading = true; - entityResource.getPagedDescendants($scope.startNodeId, "Media", $scope.searchOptions) - .then(function (data) { - // update image data to work with image grid - angular.forEach(data.items, function(mediaItem){ - // set thumbnail and src - mediaItem.thumbnail = mediaHelper.resolveFileFromEntity(mediaItem, true); - mediaItem.image = mediaHelper.resolveFileFromEntity(mediaItem, false); - // set properties to match a media object - if (mediaItem.metaData && - mediaItem.metaData.umbracoWidth && - mediaItem.metaData.umbracoHeight) { - - mediaItem.properties = [ - { - alias: "umbracoWidth", - value: mediaItem.metaData.umbracoWidth.Value - }, - { - alias: "umbracoHeight", - value: mediaItem.metaData.umbracoHeight.Value - } - ]; - } - }); - // update images - $scope.images = data.items ? data.items : []; - // update pagination - if (data.pageNumber > 0) - $scope.searchOptions.pageNumber = data.pageNumber; - if (data.pageSize > 0) - $scope.searchOptions.pageSize = data.pageSize; - $scope.searchOptions.totalItems = data.totalItems; - $scope.searchOptions.totalPages = data.totalPages; - // set already selected images to selected - preSelectImages(); - $scope.loading = false; - }); - } - - function getChildren(id) { - $scope.loading = true; - mediaResource.getChildren(id) - .then(function(data) { - $scope.searchOptions.filter = ""; - $scope.images = data.items ? data.items : []; - // set already selected images to selected - preSelectImages(); - $scope.loading = false; - }); - } - - function preSelectImages() { - for (var folderImageIndex = 0; folderImageIndex < $scope.images.length; folderImageIndex++) { - var folderImage = $scope.images[folderImageIndex]; - var imageIsSelected = false; - - if ($scope.model && angular.isArray($scope.model.selectedImages)) { - for (var selectedImageIndex = 0; - selectedImageIndex < $scope.model.selectedImages.length; - selectedImageIndex++) { - var selectedImage = $scope.model.selectedImages[selectedImageIndex]; - - if (folderImage.key === selectedImage.key) { - imageIsSelected = true; - } - } - } - - - if (imageIsSelected) { - folderImage.selected = true; - } - } - } - }); - -angular.module("umbraco").controller("Umbraco.Overlays.MediaTypePickerController", - function ($scope) { - - $scope.select = function(mediatype){ - $scope.model.selectedType = mediatype; - $scope.model.submit($scope.model); - $scope.model.show = false; - } - - }); - -//used for the member picker dialog -angular.module("umbraco").controller("Umbraco.Overlays.MemberGroupPickerController", - function($scope, eventsService, entityResource, searchService, $log, localizationService) { - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMemberGroup"); - } - - $scope.dialogTreeEventHandler = $({}); - $scope.multiPicker = $scope.model.multiPicker; - - function activate() { - - if($scope.multiPicker) { - $scope.model.selectedMemberGroups = []; - } else { - $scope.model.selectedMemberGroup = ""; - } - - } - - function selectMemberGroup(id) { - $scope.model.selectedMemberGroup = id; - } - - function selectMemberGroups(id) { - $scope.model.selectedMemberGroups.push(id); - } - - /** Method used for selecting a node */ - function select(text, id) { - - if ($scope.model.multiPicker) { - selectMemberGroups(id); - } - else { - selectMemberGroup(id); - $scope.model.submit($scope.model); - } - } - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - eventsService.emit("dialogs.memberGroupPicker.select", args); - - //This is a tree node, so we don't have an entity to pass in, it will need to be looked up - //from the server in this method. - select(args.node.name, args.node.id); - - //toggle checked state - args.node.selected = args.node.selected === true ? false : true; - } - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - - activate(); - - }); - - (function() { - "use strict"; - - function MoveOverlay($scope, localizationService, eventsService, entityHelper) { - - var vm = this; - - vm.hideSearch = hideSearch; - vm.selectResult = selectResult; - vm.onSearchResults = onSearchResults; - - var dialogOptions = $scope.model; - var searchText = "Search..."; - var node = dialogOptions.currentNode; - - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("actions_move"); - } - - $scope.model.relateToOriginal = true; - $scope.dialogTreeEventHandler = $({}); - - vm.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; - - // get entity type based on the section - $scope.entityType = entityHelper.getEntityTypeFromSection(dialogOptions.section); - - function nodeSelectHandler(ev, args) { - - if(args && args.event) { - args.event.preventDefault(); - args.event.stopPropagation(); - } - - //eventsService.emit("editors.content.copyController.select", args); - - if ($scope.model.target) { - //un-select if there's a current one selected - $scope.model.target.selected = false; - } - - $scope.model.target = args.node; - $scope.model.target.selected = true; - - } - - function nodeExpandedHandler(ev, args) { - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - } - - function hideSearch() { - vm.searchInfo.showSearch = false; - vm.searchInfo.searchFromId = null; - vm.searchInfo.searchFromName = null; - vm.searchInfo.results = []; - } - - // method to select a search result - function selectResult(evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { event: evt, node: result }); - } - - //callback when there are search results - function onSearchResults(results) { - vm.searchInfo.results = results; - vm.searchInfo.showSearch = true; - } - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - - // Mini list view - $scope.selectListViewNode = function (node) { - node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - } - - angular.module("umbraco").controller("Umbraco.Overlays.MoveOverlay", MoveOverlay); - -})(); - -(function() { - "use strict"; - - function QueryBuilderOverlayController($scope, templateQueryResource, localizationService) { - - var everything = ""; - var myWebsite = ""; - var ascendingTranslation = ""; - var descendingTranslation = ""; - - var vm = this; - - vm.properties = []; - vm.contentTypes = []; - vm.conditions = []; - - vm.datePickerConfig = { - pickDate: true, - pickTime: false, - format: "YYYY-MM-DD" - }; - - vm.chooseSource = chooseSource; - vm.getPropertyOperators = getPropertyOperators; - vm.addFilter = addFilter; - vm.trashFilter = trashFilter; - vm.changeSortOrder = changeSortOrder; - vm.setSortProperty = setSortProperty; - vm.setContentType = setContentType; - vm.setFilterProperty = setFilterProperty; - vm.setFilterTerm = setFilterTerm; - vm.changeConstraintValue = changeConstraintValue; - vm.datePickerChange = datePickerChange; - - function onInit() { - - vm.query = { - contentType: { - name: everything - }, - source: { - name: myWebsite + $http.get('backoffice/UmbracoApi/TemplateQuery/GetContentTypes').then(function (response) { + $scope.contentTypes = response.data; + }); + $http.get('backoffice/UmbracoApi/TemplateQuery/GetFilterConditions').then(function (response) { + $scope.conditions = response.data; + }); + $scope.query = { + contentType: { name: 'Everything' }, + source: { name: 'My website' }, + filters: [{ + property: undefined, + operator: undefined + }], + sort: { + property: { + alias: '', + name: '' }, - filters: [ - { - property: undefined, - operator: undefined - } - ], - sort: { - property: { - alias: "", - name: "", - }, - direction: "ascending", //This is the value for sorting sent to server - translation: { - currentLabel: ascendingTranslation, //This is the localized UI value in the the dialog - ascending: ascendingTranslation, - descending: descendingTranslation - } - } - }; - - templateQueryResource.getAllowedProperties() - .then(function(properties) { - vm.properties = properties; - }); - - templateQueryResource.getContentTypes() - .then(function(contentTypes) { - vm.contentTypes = contentTypes; - }); - - templateQueryResource.getFilterConditions() - .then(function(conditions) { - vm.conditions = conditions; - }); - - throttledFunc(); - - } - - function chooseSource(query) { - vm.contentPickerOverlay = { - view: "contentpicker", - show: true, - submit: function(model) { - - var selectedNodeId = model.selection[0].id; - var selectedNodeName = model.selection[0].name; - - if (selectedNodeId > 0) { - query.source = { id: selectedNodeId, name: selectedNodeName }; + direction: 'ascending' + } + }; + $scope.chooseSource = function (query) { + dialogService.contentPicker({ + callback: function (data) { + if (data.id > 0) { + query.source = { + id: data.id, + name: data.name + }; } else { - query.source.name = myWebsite; + query.source.name = 'My website'; delete query.source.id; } - - throttledFunc(); - - vm.contentPickerOverlay.show = false; - vm.contentPickerOverlay = null; - }, - close: function(oldModel) { - vm.contentPickerOverlay.show = false; - vm.contentPickerOverlay = null; } - }; - } - - function getPropertyOperators(property) { - var conditions = _.filter(vm.conditions, - function(condition) { - var index = condition.appliesTo.indexOf(property.type); - return index >= 0; - }); + }); + }; + var throttledFunc = _.throttle(function () { + $http.post('backoffice/UmbracoApi/TemplateQuery/PostTemplateQuery', $scope.query).then(function (response) { + $scope.result = response.data; + }); + }, 200); + $scope.$watch('query', function (value) { + throttledFunc(); + }, true); + $scope.getPropertyOperators = function (property) { + var conditions = _.filter($scope.conditions, function (condition) { + var index = condition.appliesTo.indexOf(property.type); + return index >= 0; + }); return conditions; - } - - function addFilter(query) { + }; + $scope.addFilter = function (query) { query.filters.push({}); - } - - function trashFilter(query, filter) { - for (var i = 0; i < query.filters.length; i++) { - if (query.filters[i] == filter) { - query.filters.splice(i, 1); - } - } - //if we remove the last one, add a new one to generate ui for it. - if (query.filters.length == 0) { - query.filters.push({}); - } - - } - - function changeSortOrder(query) { - if (query.sort.direction === "ascending") { - query.sort.direction = "descending"; - query.sort.translation.currentLabel = query.sort.translation.descending; + }; + $scope.trashFilter = function (query) { + query.filters.splice(query, 1); + }; + $scope.changeSortOrder = function (query) { + if (query.sort.direction === 'ascending') { + query.sort.direction = 'descending'; } else { - query.sort.direction = "ascending"; - query.sort.translation.currentLabel = query.sort.translation.ascending; + query.sort.direction = 'ascending'; } - throttledFunc(); - } - - function setSortProperty(query, property) { + }; + $scope.setSortProperty = function (query, property) { query.sort.property = property; - if (property.type === "datetime") { - query.sort.direction = "descending"; - query.sort.translation.currentLabel = query.sort.translation.descending; + if (property.type === 'datetime') { + query.sort.direction = 'descending'; } else { - query.sort.direction = "ascending"; - query.sort.translation.currentLabel = query.sort.translation.ascending; + query.sort.direction = 'ascending'; + } + }; + }); + angular.module('umbraco').controller('Umbraco.Dialogs.Template.SnippetController', function ($scope) { + $scope.type = $scope.dialogOptions.type; + $scope.section = { + name: '', + required: false + }; + }); + //used for the media picker dialog + angular.module('umbraco').controller('Umbraco.Dialogs.TreePickerController', function ($scope, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService) { + var tree = null; + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + $scope.section = dialogOptions.section; + $scope.treeAlias = dialogOptions.treeAlias; + $scope.multiPicker = dialogOptions.multiPicker; + $scope.hideHeader = true; + $scope.searchInfo = { + searchFromId: dialogOptions.startNodeId, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + //create the custom query string param for this tree + $scope.customTreeParams = dialogOptions.startNodeId ? 'startNodeId=' + dialogOptions.startNodeId : ''; + $scope.customTreeParams += dialogOptions.customTreeParams ? '&' + dialogOptions.customTreeParams : ''; + var searchText = 'Search...'; + localizationService.localize('general_search').then(function (value) { + searchText = value + '...'; + }); + // Allow the entity type to be passed in but defaults to Document for backwards compatibility. + var entityType = dialogOptions.entityType ? dialogOptions.entityType : 'Document'; + //min / max values + if (dialogOptions.minNumber) { + dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); + } + if (dialogOptions.maxNumber) { + dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); + } + if (dialogOptions.section === 'member') { + entityType = 'Member'; + } else if (dialogOptions.section === 'media') { + entityType = 'Media'; + } + //Configures filtering + if (dialogOptions.filter) { + dialogOptions.filterExclude = false; + dialogOptions.filterAdvanced = false; + //used advanced filtering + if (angular.isFunction(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } else if (angular.isObject(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } else { + if (dialogOptions.filter.startsWith('!')) { + dialogOptions.filterExclude = true; + dialogOptions.filter = dialogOptions.filter.substring(1); + } + //used advanced filtering + if (dialogOptions.filter.startsWith('{')) { + dialogOptions.filterAdvanced = true; + //convert to object + dialogOptions.filter = angular.fromJson(dialogOptions.filter); + } } - throttledFunc(); - } - - function setContentType(contentType) { - vm.query.contentType = contentType; - throttledFunc(); - } - - function setFilterProperty(filter, property) { - filter.property = property; - filter.term = {}; - filter.constraintValue = ""; } - - function setFilterTerm(filter, term) { - filter.term = term; - if (filter.constraintValue) { - throttledFunc(); + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add some custom + // children: A node to activate the search, any nodes that have already been + // selected in the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [{ + level: child.level + 1, + hasChildren: false, + parent: function () { + return child; + }, + name: searchText, + metaData: { listViewNode: child }, + cssClass: 'icon-search', + cssClasses: ['not-published'] + }]; + //add base transition classes to this node + child.cssClasses.push('tree-node-slide-up'); + var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { + return i.parentId == child.id; + }); + _.each(listViewResults, function (item) { + child.children.unshift({ + id: item.id, + name: item.name, + cssClass: 'icon umb-tree-icon sprTree ' + item.icon, + level: child.level + 1, + metaData: { isSearchResult: true }, + hasChildren: false, + parent: function () { + return child; + } + }); + }); + } + //now we need to look in the already selected search results and + // toggle the check boxes for those ones that are listed + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + if (exists) { + child.selected = true; + } + }); + //check filter + performFiltering(args.children); } } - - function changeConstraintValue() { - throttledFunc(); + //gets the tree object when it loads + function treeLoadedHandler(ev, args) { + tree = args.tree; } - - function datePickerChange(event, filter) { - if (event.date && event.date.isValid()) { - filter.constraintValue = event.date.format(vm.datePickerConfig.format); - throttledFunc(); + //wires up selection + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + $scope.searchInfo.showSearch = true; + $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + //add transition classes + var listViewNode = args.node.parent(); + listViewNode.cssClasses.push('tree-node-slide-up-hide-active'); + } else if (args.node.metaData.isSearchResult) { + //check if the item selected was a search result from a list view + //unselect + select(args.node.name, args.node.id); + //remove it from the list view children + var listView = args.node.parent(); + listView.children = _.reject(listView.children, function (child) { + return child.id == args.node.id; + }); + //remove it from the custom tracked search result list + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { + return i.id == args.node.id; + }); + } else { + eventsService.emit('dialogs.treePickerController.select', args); + if (args.node.filtered) { + return; + } + //This is a tree node, so we don't have an entity to pass in, it will need to be looked up + //from the server in this method. + select(args.node.name, args.node.id); + //toggle checked state + args.node.selected = args.node.selected === true ? false : true; } } - - var throttledFunc = _.throttle(function() { - - templateQueryResource.postTemplateQuery(vm.query) - .then(function(response) { - $scope.model.result = response; - }); - - }, - 200); - - localizationService.localizeMany([ - "template_allContent", "template_websiteRoot", "template_ascending", "template_descending" - ]) - .then(function(res) { - everything = res[0]; - myWebsite = res[1]; - ascendingTranslation = res[2]; - descendingTranslation = res[3]; - onInit(); - }); - } - - angular.module("umbraco").controller("Umbraco.Overlays.QueryBuilderController", QueryBuilderOverlayController); - -})(); -(function () { - "use strict"; - - function TemplateSectionsOverlayController($scope) { - - var vm = this; - - $scope.model.mandatoryRenderSection = false; - - if(!$scope.model.title) { - $scope.model.title = "Sections"; - } - - vm.select = select; - - function onInit() { - - if($scope.model.hasMaster) { - $scope.model.insertType = 'addSection'; + /** Method used for selecting a node */ + function select(text, id, entity) { + //if we get the root, we just return a constructed entity, no need for server data + if (id < 0) { + if ($scope.multiPicker) { + $scope.select(id); + } else { + var node = { + alias: null, + icon: 'icon-folder', + id: id, + name: text + }; + $scope.submit(node); + } } else { - $scope.model.insertType = 'renderBody'; + if ($scope.multiPicker) { + $scope.select(Number(id)); + } else { + $scope.hideSearch(); + //if an entity has been passed in, use it + if (entity) { + $scope.submit(entity); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, entityType).then(function (ent) { + $scope.submit(ent); + }); + } + } } - } - - function select(type) { - $scope.model.insertType = type; + function performFiltering(nodes) { + if (!dialogOptions.filter) { + return; + } + //remove any list view search nodes from being filtered since these are special nodes that always must + // be allowed to be clicked on + nodes = _.filter(nodes, function (n) { + return !angular.isObject(n.metaData.listViewNode); + }); + if (dialogOptions.filterAdvanced) { + //filter either based on a method or an object + var filtered = angular.isFunction(dialogOptions.filter) ? _.filter(nodes, dialogOptions.filter) : _.where(nodes, dialogOptions.filter); + angular.forEach(filtered, function (value, key) { + value.filtered = true; + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); + } + }); + } else { + var a = dialogOptions.filter.toLowerCase().replace(/\s/g, '').split(','); + angular.forEach(nodes, function (value, key) { + var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; + if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { + value.filtered = true; + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); + } + } + }); + } } - - onInit(); - - } - - angular.module("umbraco").controller("Umbraco.Overlays.TemplateSectionsOverlay", TemplateSectionsOverlayController); -})(); - -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", - function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { - - var tree = null; - var dialogOptions = $scope.model; - $scope.treeReady = false; - $scope.dialogTreeEventHandler = $({}); - $scope.section = dialogOptions.section; - $scope.treeAlias = dialogOptions.treeAlias; - $scope.multiPicker = dialogOptions.multiPicker; - $scope.hideHeader = true; - // if you need to load a not initialized tree set this value to false - default is true - $scope.onlyInitialized = dialogOptions.onlyInitialized; - $scope.searchInfo = { - searchFromId: dialogOptions.startNodeId, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - $scope.model.selection = []; - - //Used for toggling an empty-state message - //Some trees can have no items (dictionary & forms email templates) - $scope.hasItems = true; - $scope.emptyStateMessage = dialogOptions.emptyStateMessage; - - - //TODO: I don't think this is used or called anywhere!! - $scope.init = function (contentType) { - - if (contentType === "content") { - $scope.entityType = "Document"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectContent"); - } - } else if (contentType === "member") { - $scope.entityType = "Member"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMember"); - } - } else if (contentType === "media") { - $scope.entityType = "Media"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); - } - } - } - - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - // Allow the entity type to be passed in but defaults to Document for backwards compatibility. - $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; - - - //min / max values - if (dialogOptions.minNumber) { - dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); - } - if (dialogOptions.maxNumber) { - dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); - } - - if (dialogOptions.section === "member") { - $scope.entityType = "Member"; - } - else if (dialogOptions.section === "media") { - $scope.entityType = "Media"; - } - - // Search and listviews is only working for content, media and member section - var searchableSections = ["content", "media", "member"]; - - $scope.enableSearh = searchableSections.indexOf($scope.section) !== -1; - - //if a alternative startnode is used, we need to check if it is a container - if ($scope.enableSearh && dialogOptions.startNodeId && dialogOptions.startNodeId !== -1 && dialogOptions.startNodeId !== "-1") { - entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function(node) { - if (node.metaData.IsContainer) { - openMiniListView(node); - } - initTree(); - }); - } - else { - initTree(); - } - - //Configures filtering - if (dialogOptions.filter) { - - dialogOptions.filterExclude = false; - dialogOptions.filterAdvanced = false; - - //used advanced filtering - if (angular.isFunction(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else if (angular.isObject(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else { - if (dialogOptions.filter.startsWith("!")) { - dialogOptions.filterExclude = true; - dialogOptions.filter = dialogOptions.filter.substring(1); - } - - //used advanced filtering - if (dialogOptions.filter.startsWith("{")) { - dialogOptions.filterAdvanced = true; - //convert to object - dialogOptions.filter = angular.fromJson(dialogOptions.filter); - } - } - } - - function initTree() { - //create the custom query string param for this tree - $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; - $scope.customTreeParams += dialogOptions.customTreeParams ? "&" + dialogOptions.customTreeParams : ""; - $scope.treeReady = true; - } - - function nodeExpandedHandler(ev, args) { - - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - - //now we need to look in the already selected search results and - // toggle the check boxes for those ones that are listed - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - if (exists) { - child.selected = true; - } - }); - - //check filter - performFiltering(args.children); - } - } - - //gets the tree object when it loads - function treeLoadedHandler(ev, args) { - //args.tree contains children (args.tree.root.children) - $scope.hasItems = args.tree.root.children.length > 0; - - tree = args.tree; - } - - //wires up selection - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.isSearchResult) { - //check if the item selected was a search result from a list view - - //unselect - select(args.node.name, args.node.id); - - //remove it from the list view children - var listView = args.node.parent(); - listView.children = _.reject(listView.children, function (child) { - return child.id == args.node.id; - }); - - //remove it from the custom tracked search result list - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { - return i.id == args.node.id; - }); - } - else { - eventsService.emit("dialogs.treePickerController.select", args); - - if (args.node.filtered) { - return; - } - - //This is a tree node, so we don't have an entity to pass in, it will need to be looked up - //from the server in this method. - if ($scope.model.select) { - $scope.model.select(args.node) - } else { - select(args.node.name, args.node.id); - //toggle checked state - args.node.selected = args.node.selected === true ? false : true; - } - - } - } - - /** Method used for selecting a node */ - function select(text, id, entity) { - //if we get the root, we just return a constructed entity, no need for server data - if (id < 0) { - if ($scope.multiPicker) { - - if (entity) { - multiSelectItem(entity); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - multiSelectItem(ent); - }); - } - - } - else { - var node = { - alias: null, - icon: "icon-folder", - id: id, - name: text - }; - $scope.model.selection.push(node); - $scope.model.submit($scope.model); - } - } - else { - - if ($scope.multiPicker) { - - if (entity) { - multiSelectItem(entity); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - multiSelectItem(ent); - }); - } - - } - - else { - - $scope.hideSearch(); - - //if an entity has been passed in, use it - if (entity) { - $scope.model.selection.push(entity); - $scope.model.submit($scope.model); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - $scope.model.selection.push(ent); - $scope.model.submit($scope.model); - }); - } - } - } - } - - function multiSelectItem(item) { - - var found = false; - var foundIndex = 0; - - if ($scope.model.selection.length > 0) { - for (i = 0; $scope.model.selection.length > i; i++) { - var selectedItem = $scope.model.selection[i]; - if (selectedItem.id === item.id) { - found = true; - foundIndex = i; - } - } - } - - if (found) { - $scope.model.selection.splice(foundIndex, 1); - } else { - $scope.model.selection.push(item); - } - - } - - function performFiltering(nodes) { - - if (!dialogOptions.filter) { - return; - } - - //remove any list view search nodes from being filtered since these are special nodes that always must - // be allowed to be clicked on - nodes = _.filter(nodes, function (n) { - return !angular.isObject(n.metaData.listViewNode); - }); - - if (dialogOptions.filterAdvanced) { - - //filter either based on a method or an object - var filtered = angular.isFunction(dialogOptions.filter) - ? _.filter(nodes, dialogOptions.filter) - : _.where(nodes, dialogOptions.filter); - - angular.forEach(filtered, function (value, key) { - value.filtered = true; - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - }); - } else { - var a = dialogOptions.filter.toLowerCase().replace(/\s/g, '').split(','); - angular.forEach(nodes, function (value, key) { - - var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; - - if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { - value.filtered = true; - - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - } - }); - } - } - - $scope.multiSubmit = function (result) { - entityResource.getByIds(result, $scope.entityType).then(function (ents) { - $scope.submit(ents); - }); - }; - - /** method to select a search result */ - $scope.selectResult = function (evt, result) { - - if (result.filtered) { - return; - } - - result.selected = result.selected === true ? false : true; - - //since result = an entity, we'll pass it in so we don't have to go back to the server - select(result.name, result.id, result); - - //add/remove to our custom tracked list of selected search results - if (result.selected) { - $scope.searchInfo.selectedSearchResults.push(result); - } - else { - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { - return i.id == result.id; - }); - } - - //ensure the tree node in the tree is checked/unchecked if it already exists there - if (tree) { - var found = treeService.getDescendantNode(tree.root, result.id); - if (found) { - found.selected = result.selected; - } - } - - }; - - $scope.hideSearch = function () { - - //Traverse the entire displayed tree and update each node to sync with the selected search results - if (tree) { - - //we need to ensure that any currently displayed nodes that get selected - // from the search get updated to have a check box! - function checkChildren(children) { - _.each(children, function (child) { - //check if the id is in the selection, if so ensure it's flagged as selected - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - //if the curr node exists in selected search results, ensure it's checked - if (exists) { - child.selected = true; - } - //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result - else if (child.metaData.isSearchResult) { - //if this tree node is under a list view it means that the node was added - // to the tree dynamically under the list view that was searched, so we actually want to remove - // it all together from the tree - var listView = child.parent(); - listView.children = _.reject(listView.children, function (c) { - return c.id == child.id; - }); - } - - //check if the current node is a list view and if so, check if there's any new results - // that need to be added as child nodes to it based on search results selected - if (child.metaData.isContainer) { - - child.cssClasses = _.reject(child.cssClasses, function (c) { - return c === 'tree-node-slide-up-hide-active'; - }); - - var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { - return i.parentId == child.id; - }); - _.each(listViewResults, function (item) { - var childExists = _.find(child.children, function (c) { - return c.id == item.id; - }); - if (!childExists) { - var parent = child; - child.children.unshift({ - id: item.id, - name: item.name, - cssClass: "icon umb-tree-icon sprTree " + item.icon, - level: child.level + 1, - metaData: { - isSearchResult: true - }, - hasChildren: false, - parent: function () { - return parent; - } - }); - } - }); - } - - //recurse - if (child.children && child.children.length > 0) { - checkChildren(child.children); - } - }); - } - checkChildren(tree.root.children); - } - - - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = dialogOptions.startNodeId; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - $scope.onSearchResults = function (results) { - - //filter all items - this will mark an item as filtered - performFiltering(results); - - //now actually remove all filtered items so they are not even displayed - results = _.filter(results, function (item) { - return !item.filtered; - }); - - $scope.searchInfo.results = results; - - //sync with the curr selected results - _.each($scope.searchInfo.results, function (result) { - var exists = _.find($scope.model.selection, function (selectedId) { - return result.id == selectedId; - }); - if (exists) { - result.selected = true; - } - }); - - $scope.searchInfo.showSearch = true; - }; - - $scope.dialogTreeEventHandler.bind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - - $scope.selectListViewNode = function (node) { - select(node.name, node.id); - //toggle checked state - node.selected = node.selected === true ? false : true; - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - }); - -angular.module("umbraco") - .controller("Umbraco.Overlays.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper, localizationService) { - + $scope.multiSubmit = function (result) { + entityResource.getByIds(result, entityType).then(function (ents) { + $scope.submit(ents); + }); + }; + /** method to select a search result */ + $scope.selectResult = function (evt, result) { + if (result.filtered) { + return; + } + result.selected = result.selected === true ? false : true; + //since result = an entity, we'll pass it in so we don't have to go back to the server + select(result.name, result.id, result); + //add/remove to our custom tracked list of selected search results + if (result.selected) { + $scope.searchInfo.selectedSearchResults.push(result); + } else { + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { + return i.id == result.id; + }); + } + //ensure the tree node in the tree is checked/unchecked if it already exists there + if (tree) { + var found = treeService.getDescendantNode(tree.root, result.id); + if (found) { + found.selected = result.selected; + } + } + }; + $scope.hideSearch = function () { + //Traverse the entire displayed tree and update each node to sync with the selected search results + if (tree) { + //we need to ensure that any currently displayed nodes that get selected + // from the search get updated to have a check box! + function checkChildren(children) { + _.each(children, function (child) { + //check if the id is in the selection, if so ensure it's flagged as selected + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + //if the curr node exists in selected search results, ensure it's checked + if (exists) { + child.selected = true; + } //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result + else if (child.metaData.isSearchResult) { + //if this tree node is under a list view it means that the node was added + // to the tree dynamically under the list view that was searched, so we actually want to remove + // it all together from the tree + var listView = child.parent(); + listView.children = _.reject(listView.children, function (c) { + return c.id == child.id; + }); + } + //check if the current node is a list view and if so, check if there's any new results + // that need to be added as child nodes to it based on search results selected + if (child.metaData.isContainer) { + child.cssClasses = _.reject(child.cssClasses, function (c) { + return c === 'tree-node-slide-up-hide-active'; + }); + var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { + return i.parentId == child.id; + }); + _.each(listViewResults, function (item) { + var childExists = _.find(child.children, function (c) { + return c.id == item.id; + }); + if (!childExists) { + var parent = child; + child.children.unshift({ + id: item.id, + name: item.name, + cssClass: 'icon umb-tree-icon sprTree ' + item.icon, + level: child.level + 1, + metaData: { isSearchResult: true }, + hasChildren: false, + parent: function () { + return parent; + } + }); + } + }); + } + //recurse + if (child.children && child.children.length > 0) { + checkChildren(child.children); + } + }); + } + checkChildren(tree.root.children); + } + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = dialogOptions.startNodeId; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + }; + $scope.onSearchResults = function (results) { + //filter all items - this will mark an item as filtered + performFiltering(results); + //now actually remove all filtered items so they are not even displayed + results = _.filter(results, function (item) { + return !item.filtered; + }); + $scope.searchInfo.results = results; + //sync with the curr selected results + _.each($scope.searchInfo.results, function (result) { + var exists = _.find($scope.dialogData.selection, function (selectedId) { + return result.id == selectedId; + }); + if (exists) { + result.selected = true; + } + }); + $scope.searchInfo.showSearch = true; + }; + $scope.dialogTreeEventHandler.bind('treeLoaded', treeLoadedHandler); + $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeLoaded', treeLoadedHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + }); + }); + angular.module('umbraco').controller('Umbraco.Dialogs.UserController', function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper) { $scope.history = historyService.getCurrent(); - $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.version = Umbraco.Sys.ServerVariables.application.version + ' assembly: ' + Umbraco.Sys.ServerVariables.application.assemblyVersion; $scope.showPasswordFields = false; - $scope.changePasswordButtonState = "init"; - $scope.model.subtitle = "Umbraco version" + " " + $scope.version; - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("general_user"); - } - + $scope.changePasswordButtonState = 'init'; $scope.externalLoginProviders = externalLoginInfo.providers; $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; var evts = []; - evts.push(eventsService.on("historyService.add", function (e, args) { + evts.push(eventsService.on('historyService.add', function (e, args) { $scope.history = args.all; })); - evts.push(eventsService.on("historyService.remove", function (e, args) { + evts.push(eventsService.on('historyService.remove', function (e, args) { $scope.history = args.all; })); - evts.push(eventsService.on("historyService.removeAll", function (e, args) { + evts.push(eventsService.on('historyService.removeAll', function (e, args) { $scope.history = []; })); - $scope.logout = function () { - //Add event listener for when there are pending changes on an editor which means our route was not successful - var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { + var pendingChangeEvent = eventsService.on('valFormManager.pendingChanges', function (e, args) { //one time listener, remove the event pendingChangeEvent(); - $scope.model.close(); + $scope.close(); }); - - //perform the path change, if it is successful then the promise will resolve otherwise it will fail - $scope.model.close(); - $location.path("/logout"); + $scope.close(); + $location.path('/logout'); }; - $scope.gotoHistory = function (link) { $location.path(link); - $scope.model.close(); + $scope.close(); }; - //Manually update the remaining timeout seconds function updateTimeout() { $timeout(function () { @@ -5083,27 +1898,22 @@ angular.module("umbraco") //recurse updateTimeout(); } - - }, 1000, false); // 1 second, do NOT execute a global digest + }, 1000, false); // 1 second, do NOT execute a global digest } - function updateUserInfo() { //get the user userService.getCurrentUser().then(function (user) { $scope.user = user; if ($scope.user) { - $scope.model.title = user.name; $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; + $scope.canEditProfile = _.indexOf($scope.user.allowedSections, 'users') > -1; //set the timer updateTimeout(); - - authResource.getCurrentUserLinkedLogins().then(function(logins) { + authResource.getCurrentUserLinkedLogins().then(function (logins) { //reset all to be un-linked for (var provider in $scope.externalLoginProviders) { $scope.externalLoginProviders[provider].linkedProviderKey = undefined; } - //set the linked logins for (var login in logins) { var found = _.find($scope.externalLoginProviders, function (i) { @@ -5117,13922 +1927,16107 @@ angular.module("umbraco") } }); } - $scope.unlink = function (e, loginProvider, providerKey) { - var result = confirm("Are you sure you want to unlink this account?"); + var result = confirm('Are you sure you want to unlink this account?'); if (!result) { e.preventDefault(); return; } - authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { updateUserInfo(); }); - } - + }; updateUserInfo(); - //remove all event handlers $scope.$on('$destroy', function () { for (var e = 0; e < evts.length; e++) { evts[e](); } - }); - /* ---------- UPDATE PASSWORD ---------- */ - //create the initial model for change password property editor $scope.changePasswordModel = { - alias: "_umb_password", - view: "changepassword", - config: {}, - value: {} + alias: '_umb_password', + view: 'changepassword', + config: {}, + value: {} }; - //go get the config for the membership provider and add it to the model - currentUserResource.getMembershipProviderConfig().then(function(data) { - $scope.changePasswordModel.config = data; - //ensure the hasPassword config option is set to true (the user of course has a password already assigned) - //this will ensure the oldPassword is shown so they can change it - // disable reset password functionality beacuse it does not make sense inside the backoffice - $scope.changePasswordModel.config.hasPassword = true; - $scope.changePasswordModel.config.disableToggle = true; - $scope.changePasswordModel.config.enableReset = false; + authResource.getMembershipProviderConfig().then(function (data) { + $scope.changePasswordModel.config = data; + //ensure the hasPassword config option is set to true (the user of course has a password already assigned) + //this will ensure the oldPassword is shown so they can change it + // disable reset password functionality beacuse it does not make sense for the current user. + $scope.changePasswordModel.config.hasPassword = true; + $scope.changePasswordModel.config.disableToggle = true; + $scope.changePasswordModel.config.enableReset = false; }); - - $scope.changePassword = function() { - - if (formHelper.submitForm({ scope: $scope })) { - - $scope.changePasswordButtonState = "busy"; - - currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { - + $scope.changePassword = function () { + if (formHelper.submitForm({ scope: $scope })) { + $scope.changePasswordButtonState = 'busy'; + currentUserResource.changePassword($scope.changePasswordModel.value).then(function (data) { //if the password has been reset, then update our model if (data.value) { $scope.changePasswordModel.value.generatedPassword = data.value; } - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - $scope.changePasswordButtonState = "success"; - $timeout(function() { - $scope.togglePasswordFields(); - }, 2000); - + formHelper.resetForm({ + scope: $scope, + notifications: data.notifications + }); + $scope.changePasswordButtonState = 'success'; }, function (err) { - formHelper.handleError(err); - - $scope.changePasswordButtonState = "error"; - + $scope.changePasswordButtonState = 'error'; }); - } - }; - - $scope.togglePasswordFields = function() { - clearPasswordFields(); - $scope.showPasswordFields = !$scope.showPasswordFields; - } - + $scope.togglePasswordFields = function () { + clearPasswordFields(); + $scope.showPasswordFields = !$scope.showPasswordFields; + }; function clearPasswordFields() { - $scope.changePasswordModel.value.newPassword = ""; - $scope.changePasswordModel.confirm = ""; + $scope.changePasswordModel.value.newPassword = ''; + $scope.changePasswordModel.confirm = ''; } - }); - -angular.module("umbraco") - .controller("Umbraco.Overlays.YsodController", function ($scope, legacyResource, treeService, navigationService, localizationService) { - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("errors_receivedErrorFromServer"); - } - - if ($scope.model.error && $scope.model.error.data && $scope.model.error.data.StackTrace) { - //trim whitespace - $scope.model.error.data.StackTrace = $scope.model.error.data.StackTrace.trim(); - } - - if ($scope.model.error && $scope.model.error.data) { - $scope.model.error.data.InnerExceptions = []; - var ex = $scope.model.error.data.InnerException; - while (ex) { - if (ex.StackTrace) { - ex.StackTrace = ex.StackTrace.trim(); - } - $scope.model.error.data.InnerExceptions.push(ex); - ex = ex.InnerException; - } - } - - }); - -angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { - - var dialogOptions = $scope.dialogOptions; - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - $scope.relateToOriginal = true; - $scope.recursive = true; - $scope.dialogTreeEventHandler = $({}); - $scope.busy = false; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - var node = dialogOptions.currentNode; - - function nodeSelectHandler(ev, args) { - - if(args && args.event) { - args.event.preventDefault(); - args.event.stopPropagation(); - } - - eventsService.emit("editors.content.copyController.select", args); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - - } - - function nodeExpandedHandler(ev, args) { - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - } - - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { event: evt, node: result }); - }; - - //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; - $scope.searchInfo.showSearch = true; - }; - - $scope.copy = function () { - - $scope.busy = true; - $scope.error = false; - - contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the copied content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was copied!!) - - navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "content", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - - // Mini list view - $scope.selectListViewNode = function (node) { - node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - }); - -/** + /** * @ngdoc controller - * @name Umbraco.Editors.Content.CreateController + * @name Umbraco.Dialogs.LegacyDeleteController * @function * * @description - * The controller for the content creation dialog - */ -function contentCreateController($scope, $routeParams, contentTypeResource, iconHelper) { - - contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function(data) { - $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); - }); -} - -angular.module('umbraco').controller("Umbraco.Editors.Content.CreateController", contentCreateController); -/** + * The controller for deleting content + */ + function YsodController($scope, legacyResource, treeService, navigationService) { + if ($scope.error && $scope.error.data && $scope.error.data.StackTrace) { + //trim whitespace + $scope.error.data.StackTrace = $scope.error.data.StackTrace.trim(); + } + $scope.closeDialog = function () { + $scope.dismiss(); + }; + } + angular.module('umbraco').controller('Umbraco.Dialogs.YsodController', YsodController); + /** * @ngdoc controller - * @name Umbraco.Editors.ContentDeleteController + * @name Umbraco.LegacyController * @function * * @description - * The controller for deleting content - */ -function ContentDeleteController($scope, contentResource, treeService, navigationService, editorState, $location, dialogService, notificationsService) { - - $scope.performDelete = function() { - - // stop from firing again on double-click - if ($scope.busy) { return false; } - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - $scope.busy = true; - - contentResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - treeService.removeNode($scope.currentNode); - - if (rootNode) { - //ensure the recycle bin has child nodes now - var recycleBin = treeService.getDescendantNode(rootNode, -20); - if (recycleBin) { - recycleBin.hasChildren = true; - } - } - - //if the current edited item is the same one as we're deleting, we need to navigate elsewhere - if (editorState.current && editorState.current.id == $scope.currentNode.id) { - - //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent - var location = "/content"; - if ($scope.currentNode.parentId.toString() !== "-1") - location = "/content/content/edit/" + $scope.currentNode.parentId; - - $location.path(location); - } - - navigationService.hideMenu(); - }, function(err) { - - $scope.currentNode.loading = false; - $scope.busy = false; - - //check if response is ysod - if (err.status && err.status >= 500) { - dialogService.ysodDialog(err); - } - - if (err.data && angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Content.DeleteController", ContentDeleteController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.Content.EditController - * @function - * - * @description - * The controller for the content editor - */ -function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { - - //setup scope vars - $scope.defaultButton = null; - $scope.subButtons = []; - - $scope.page = {}; - $scope.page.loading = false; - $scope.page.menu = {}; - $scope.page.menu.currentNode = null; - $scope.page.menu.currentSection = appState.getSectionState("currentSection"); - $scope.page.listViewPath = null; - $scope.page.isNew = $routeParams.create; - $scope.page.buttonGroupState = "init"; - - function init(content) { - - var buttons = contentEditingHelper.configureContentEditorButtons({ - create: $routeParams.create, - content: content, - methods: { - saveAndPublish: $scope.saveAndPublish, - sendToPublish: $scope.sendToPublish, - save: $scope.save, - unPublish: $scope.unPublish - } - }); - $scope.defaultButton = buttons.defaultButton; - $scope.subButtons = buttons.subButtons; - - editorState.set($scope.content); - - //We fetch all ancestors of the node to generate the footer breadcrumb navigation - if (!$routeParams.create) { - if (content.parentId && content.parentId != -1) { - entityResource.getAncestors(content.id, "document") - .then(function (anc) { - $scope.ancestors = anc; - }); + * A controller to control the legacy iframe injection + * +*/ + function LegacyController($scope, $routeParams, $element) { + var url = decodeURIComponent($routeParams.url.replace(/javascript\:/gi, '')); + //split into path and query + var urlParts = url.split('?'); + var extIndex = urlParts[0].lastIndexOf('.'); + var ext = extIndex === -1 ? '' : urlParts[0].substr(extIndex); + //path cannot be a js file + if (ext !== '.js' || ext === '') { + //path cannot contain any of these chars + var toClean = '*(){}[];:<>\\|\'"'; + for (var i = 0; i < toClean.length; i++) { + var reg = new RegExp('\\' + toClean[i], 'g'); + urlParts[0] = urlParts[0].replace(reg, ''); } + //join cleaned path and query back together + url = urlParts[0] + (urlParts.length === 1 ? '' : '?' + urlParts[1]); + $scope.legacyPath = url; + } else { + throw 'Invalid url'; } } - - /** Syncs the content item to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(content, path, initialLoad) { - - if (!$scope.content.isChildOfListView) { - navigationService.syncTree({ tree: "content", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; - }); - } - else if (initialLoad === true) { - - //it's a child item, just sync the ui node to the parent - navigationService.syncTree({ tree: "content", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node - // from the server so that we can load in the actions menu. - umbRequestHelper.resourcePromise( - $http.get(content.treeNodeUrl), - 'Failed to retrieve data for child node ' + content.id).then(function (node) { - $scope.page.menu.currentNode = node; - }); - } - } - - // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish - function performSave(args) { - var deferred = $q.defer(); - - $scope.page.buttonGroupState = "busy"; - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: args.statusMessage, - saveMethod: args.saveMethod, - scope: $scope, - content: $scope.content, - action: args.action - }).then(function (data) { - //success - init($scope.content); - syncTreeNode($scope.content, data.path); - - $scope.page.buttonGroupState = "success"; - - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); + angular.module('umbraco').controller('Umbraco.LegacyController', LegacyController); + /** This controller is simply here to launch the login dialog when the route is explicitly changed to /login */ + angular.module('umbraco').controller('Umbraco.LoginController', function (eventsService, $scope, userService, $location, $rootScope) { + userService._showLoginDialog(); + var evtOn = eventsService.on('app.ready', function (evt, data) { + $scope.avatar = 'assets/img/application/logo.png'; + var path = '/'; + //check if there's a returnPath query string, if so redirect to it + var locationObj = $location.search(); + if (locationObj.returnPath) { + path = decodeURIComponent(locationObj.returnPath); } - - $scope.page.buttonGroupState = "error"; - - deferred.reject(err); + $location.url(path); }); - - return deferred.promise; - } - - function resetLastListPageNumber(content) { - // We're using rootScope to store the page number for list views, so if returning to the list - // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children - // we should remove this so as not to confuse if navigating to a different list - if (!content.isChildOfListView && !content.isContainer) { - $rootScope.lastListViewPageViewed = null; + $scope.$on('$destroy', function () { + eventsService.unsubscribe(evtOn); + }); + }); + //used for the media picker dialog + angular.module('umbraco').controller('Umbraco.Notifications.ConfirmRouteChangeController', function ($scope, $location, $log, notificationsService) { + $scope.discard = function (not) { + not.args.listener(); + $location.search(''); + //we need to break the path up into path and query + var parts = not.args.path.split('?'); + var query = {}; + if (parts.length > 1) { + _.each(parts[1].split('&'), function (q) { + var keyVal = q.split('='); + query[keyVal[0]] = keyVal[1]; + }); + } + $location.path(parts[0]).search(query); + notificationsService.remove(not); + }; + $scope.stay = function (not) { + notificationsService.remove(not); + }; + }); + (function () { + 'use strict'; + function CompositionsOverlay($scope) { + var vm = this; + vm.isSelected = isSelected; + function isSelected(alias) { + if ($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { + return true; + } + } } - } - - if ($routeParams.create) { - - $scope.page.loading = true; - - //we are creating so get an empty content item - contentResource.getScaffold($routeParams.id, $routeParams.doctype) - .then(function (data) { - - $scope.content = data; - - init($scope.content); - - resetLastListPageNumber($scope.content); - - $scope.page.loading = false; - - }); - } - else { - - $scope.page.loading = true; - - //we are editing so get the content item from the server - contentResource.getById($routeParams.id) - .then(function (data) { - - $scope.content = data; - - if (data.isChildOfListView && data.trashed === false) { - $scope.page.listViewPath = ($routeParams.page) - ? "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page - : "/content/content/edit/" + data.parentId; + angular.module('umbraco').controller('Umbraco.Overlays.CompositionsOverlay', CompositionsOverlay); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ + (function () { + 'use strict'; + function EditorPickerOverlay($scope, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService) { + var vm = this; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectEditor'); + } + vm.searchTerm = ''; + vm.showTabs = false; + vm.tabsLoaded = 0; + vm.typesAndEditors = []; + vm.userConfigured = []; + vm.loading = false; + vm.tabs = [ + { + active: true, + id: 1, + label: localizationService.localize('contentTypeEditor_availableEditors'), + alias: 'Default', + typesAndEditors: [] + }, + { + active: false, + id: 2, + label: localizationService.localize('contentTypeEditor_reuse'), + alias: 'Reuse', + userConfigured: [] } - - init($scope.content); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - syncTreeNode($scope.content, data.path, true); - - resetLastListPageNumber($scope.content); - - $scope.page.loading = false; - - }); - } - - - $scope.unPublish = function () { - - if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) { - - $scope.page.buttonGroupState = "busy"; - - contentResource.unPublish($scope.content.id) - .then(function (data) { - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + ]; + vm.filterItems = filterItems; + vm.showDetailsOverlay = showDetailsOverlay; + vm.hideDetailsOverlay = hideDetailsOverlay; + vm.pickEditor = pickEditor; + vm.pickDataType = pickDataType; + function activate() { + getGroupedDataTypes(); + getGroupedPropertyEditors(); + } + function getGroupedPropertyEditors() { + vm.loading = true; + dataTypeResource.getGroupedPropertyEditors().then(function (data) { + vm.tabs[0].typesAndEditors = data; + vm.typesAndEditors = data; + vm.tabsLoaded = vm.tabsLoaded + 1; + checkIfTabContentIsLoaded(); + }); + } + function getGroupedDataTypes() { + vm.loading = true; + dataTypeResource.getGroupedDataTypes().then(function (data) { + vm.tabs[1].userConfigured = data; + vm.userConfigured = data; + vm.tabsLoaded = vm.tabsLoaded + 1; + checkIfTabContentIsLoaded(); + }); + } + function checkIfTabContentIsLoaded() { + if (vm.tabsLoaded === 2) { + vm.loading = false; + vm.showTabs = true; + } + } + function filterItems() { + // clear item details + $scope.model.itemDetails = null; + if (vm.searchTerm) { + vm.showFilterResult = true; + vm.showTabs = false; + } else { + vm.showFilterResult = false; + vm.showTabs = true; + } + } + function showDetailsOverlay(property) { + var propertyDetails = {}; + propertyDetails.icon = property.icon; + propertyDetails.title = property.name; + $scope.model.itemDetails = propertyDetails; + } + function hideDetailsOverlay() { + $scope.model.itemDetails = null; + } + function pickEditor(editor) { + var parentId = -1; + dataTypeResource.getScaffold(parentId).then(function (dataType) { + // set alias + dataType.selectedEditor = editor.alias; + // set name + var nameArray = []; + if ($scope.model.contentTypeName) { + nameArray.push($scope.model.contentTypeName); + } + if ($scope.model.property.label) { + nameArray.push($scope.model.property.label); + } + if (editor.name) { + nameArray.push(editor.name); + } + // make name + dataType.name = nameArray.join(' - '); + // get pre values + dataTypeResource.getPreValues(dataType.selectedEditor).then(function (preValues) { + dataType.preValues = preValues; + openEditorSettingsOverlay(dataType, true); }); - - init($scope.content); - - syncTreeNode($scope.content, data.path); - - $scope.page.buttonGroupState = "success"; - - }); - } - - }; - - $scope.sendToPublish = function () { - return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); - }; - - $scope.saveAndPublish = function () { - return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); - }; - - $scope.save = function () { - return performSave({ saveMethod: contentResource.save, statusMessage: "Saving...", action: "save" }); - }; - - $scope.preview = function (content) { - - - if (!$scope.busy) { - - // Chromes popup blocker will kick in if a window is opened - // outwith the initial scoped request. This trick will fix that. - // - var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); - $scope.save().then(function (data) { - // Build the correct path so both /#/ and #/ work. - var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + data.id; - previewWindow.location.href = redirect; - }); - - - } - - }; - -} - -angular.module("umbraco").controller("Umbraco.Editors.Content.EditController", ContentEditController); - -/** + }); + } + function pickDataType(selectedDataType) { + dataTypeResource.getById(selectedDataType.id).then(function (dataType) { + contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function (propertyType) { + submitOverlay(dataType, propertyType, false); + }); + }); + } + function openEditorSettingsOverlay(dataType, isNew) { + vm.editorSettingsOverlay = { + title: localizationService.localize('contentTypeEditor_editorSettings'), + dataType: dataType, + view: 'views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html', + show: true, + submit: function (model) { + var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); + dataTypeResource.save(model.dataType, preValues, isNew).then(function (newDataType) { + contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function (propertyType) { + submitOverlay(newDataType, propertyType, true); + vm.editorSettingsOverlay.show = false; + vm.editorSettingsOverlay = null; + }); + }); + } + }; + } + function submitOverlay(dataType, propertyType, isNew) { + // update property + $scope.model.property.config = propertyType.config; + $scope.model.property.editor = propertyType.editor; + $scope.model.property.view = propertyType.view; + $scope.model.property.dataTypeId = dataType.id; + $scope.model.property.dataTypeIcon = dataType.icon; + $scope.model.property.dataTypeName = dataType.name; + $scope.model.updateSameDataTypes = isNew; + $scope.model.submit($scope.model); + } + activate(); + } + angular.module('umbraco').controller('Umbraco.Overlays.EditorPickerOverlay', EditorPickerOverlay); + }()); + /** * @ngdoc controller - * @name Umbraco.Editors.Content.EmptyRecycleBinController + * @name Umbraco.Editors.DocumentType.PropertyController * @function - * + * * @description - * The controller for deleting content - */ -function ContentEmptyRecycleBinController($scope, contentResource, treeService, navigationService, notificationsService, $route) { - - $scope.busy = false; - - $scope.performDelete = function() { - - //(used in the UI) - $scope.busy = true; - $scope.currentNode.loading = true; - - contentResource.emptyRecycleBin($scope.currentNode.id).then(function (result) { - - $scope.busy = false; - $scope.currentNode.loading = false; - - //show any notifications - if (angular.isArray(result.notifications)) { - for (var i = 0; i < result.notifications.length; i++) { - notificationsService.showNotification(result.notifications[i]); - } - } - - treeService.removeChildNodes($scope.currentNode); - navigationService.hideMenu(); - - //reload the current view - $route.reload(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Content.EmptyRecycleBinController", ContentEmptyRecycleBinController); - -angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { - - var dialogOptions = $scope.dialogOptions; - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - $scope.dialogTreeEventHandler = $({}); - $scope.busy = false; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - var node = dialogOptions.currentNode; - - function nodeSelectHandler(ev, args) { - - if(args && args.event) { - args.event.preventDefault(); - args.event.stopPropagation(); - } - - eventsService.emit("editors.content.moveController.select", args); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - - } - - function nodeExpandedHandler(ev, args) { - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - } - - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { event: evt, node: result }); - }; - - //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; - $scope.searchInfo.showSearch = true; - }; - - $scope.move = function () { - - $scope.busy = true; - $scope.error = false; - - contentResource.move({ parentId: $scope.target.id, id: node.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currently edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "content", path: activeNodePath, forceReload: false, activate: true }); + * The controller for the content type editor property dialog + */ + (function () { + 'use strict'; + function PropertySettingsOverlay($scope, contentTypeResource, dataTypeResource, dataTypeHelper, localizationService) { + var vm = this; + vm.showValidationPattern = false; + vm.focusOnPatternField = false; + vm.focusOnMandatoryField = false; + vm.selectedValidationType = {}; + vm.validationTypes = [ + { + 'name': localizationService.localize('validation_validateAsEmail'), + 'key': 'email', + 'pattern': '[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+', + 'enableEditing': true + }, + { + 'name': localizationService.localize('validation_validateAsNumber'), + 'key': 'number', + 'pattern': '^[0-9]*$', + 'enableEditing': true + }, + { + 'name': localizationService.localize('validation_validateAsUrl'), + 'key': 'url', + 'pattern': 'https?://[a-zA-Z0-9-.]+.[a-zA-Z]{2,}', + 'enableEditing': true + }, + { + 'name': localizationService.localize('validation_enterCustomValidation'), + 'key': 'custom', + 'pattern': '', + 'enableEditing': true + } + ]; + vm.changeValidationType = changeValidationType; + vm.changeValidationPattern = changeValidationPattern; + vm.openEditorPickerOverlay = openEditorPickerOverlay; + vm.openEditorSettingsOverlay = openEditorSettingsOverlay; + function activate() { + matchValidationType(); + } + function changeValidationPattern() { + matchValidationType(); + } + function openEditorPickerOverlay(property) { + vm.focusOnMandatoryField = false; + vm.editorPickerOverlay = {}; + vm.editorPickerOverlay.property = $scope.model.property; + vm.editorPickerOverlay.contentTypeName = $scope.model.contentTypeName; + vm.editorPickerOverlay.view = 'views/common/overlays/contenttypeeditor/editorpicker/editorpicker.html'; + vm.editorPickerOverlay.show = true; + vm.editorPickerOverlay.submit = function (model) { + $scope.model.updateSameDataTypes = model.updateSameDataTypes; + vm.focusOnMandatoryField = true; + // update property + property.config = model.property.config; + property.editor = model.property.editor; + property.view = model.property.view; + property.dataTypeId = model.property.dataTypeId; + property.dataTypeIcon = model.property.dataTypeIcon; + property.dataTypeName = model.property.dataTypeName; + vm.editorPickerOverlay.show = false; + vm.editorPickerOverlay = null; + }; + vm.editorPickerOverlay.close = function (model) { + vm.editorPickerOverlay.show = false; + vm.editorPickerOverlay = null; + }; + } + function openEditorSettingsOverlay(property) { + vm.focusOnMandatoryField = false; + // get data type + dataTypeResource.getById(property.dataTypeId).then(function (dataType) { + vm.editorSettingsOverlay = {}; + vm.editorSettingsOverlay.title = 'Editor settings'; + vm.editorSettingsOverlay.view = 'views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html'; + vm.editorSettingsOverlay.dataType = dataType; + vm.editorSettingsOverlay.show = true; + vm.editorSettingsOverlay.submit = function (model) { + var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); + dataTypeResource.save(model.dataType, preValues, false).then(function (newDataType) { + contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function (propertyType) { + // update editor + property.config = propertyType.config; + property.editor = propertyType.editor; + property.view = propertyType.view; + property.dataTypeId = newDataType.id; + property.dataTypeIcon = newDataType.icon; + property.dataTypeName = newDataType.name; + // set flag to update same data types + $scope.model.updateSameDataTypes = true; + vm.focusOnMandatoryField = true; + vm.editorSettingsOverlay.show = false; + vm.editorSettingsOverlay = null; + }); + }); + }; + vm.editorSettingsOverlay.close = function (oldModel) { + vm.editorSettingsOverlay.show = false; + vm.editorSettingsOverlay = null; + }; + }); + } + function matchValidationType() { + if ($scope.model.property.validation.pattern !== null && $scope.model.property.validation.pattern !== '' && $scope.model.property.validation.pattern !== undefined) { + var match = false; + // find and show if a match from the list has been chosen + angular.forEach(vm.validationTypes, function (validationType, index) { + if ($scope.model.property.validation.pattern === validationType.pattern) { + vm.selectedValidationType = vm.validationTypes[index]; + vm.showValidationPattern = true; + match = true; } }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - - // Mini list view - $scope.selectListViewNode = function (node) { - node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - }); -/** - * @ngdoc controller - * @name Umbraco.Editors.Content.RecycleBinController - * @function - * - * @description - * Controls the recycle bin for content - * - */ - -function ContentRecycleBinController($scope, $routeParams, contentResource, navigationService, localizationService) { - - //ensures the list view doesn't actually load until we query for the list view config - // for the section - $scope.page = {}; - $scope.page.name = "Recycle Bin"; - $scope.page.nameLocked = true; - - //ensures the list view doesn't actually load until we query for the list view config - // for the section - $scope.listViewPath = null; - - $routeParams.id = "-20"; - contentResource.getRecycleBin().then(function (result) { - //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a - // single property, so we'll extract that property (list view) and use it's data. - var listproperty = result.tabs[0].properties[0]; - - _.each(listproperty.config, function (val, key) { - $scope.model.config[key] = val; - }); - $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; - }); - - $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; - - // sync tree node - navigationService.syncTree({ tree: "content", path: ["-1", $routeParams.id], forceReload: false }); - - localizePageName(); - - function localizePageName() { - - var pageName = "general_recycleBin"; - - localizationService.localize(pageName).then(function (value) { - $scope.page.name = value; - }); - - } -} - -angular.module('umbraco').controller("Umbraco.Editors.Content.RecycleBinController", ContentRecycleBinController); - -angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController", - function ($scope, relationResource, contentResource, navigationService, appState, treeService) { - var dialogOptions = $scope.dialogOptions; - - var node = dialogOptions.currentNode; - - $scope.error = null; - $scope.success = false; - - relationResource.getByChildId(node.id, "relateParentDocumentOnDelete").then(function (data) { - - if (data.length == 0) { - $scope.success = false; - $scope.error = { - errorMsg: "Cannot automatically restore this item", - data: { - Message: "There is no 'restore' relation found for this node. Use the Move menu item to move it manually." + // if there is no match - choose the custom validation option. + if (!match) { + angular.forEach(vm.validationTypes, function (validationType) { + if (validationType.key === 'custom') { + vm.selectedValidationType = validationType; + vm.showValidationPattern = true; + } + }); } } - return; } - - $scope.relation = data[0]; - - if ($scope.relation.parentId == -1) { - $scope.target = { id: -1, name: "Root" }; - - } else { - contentResource.getById($scope.relation.parentId).then(function (data) { - $scope.target = data; - - }, function (err) { - $scope.success = false; - $scope.error = err; - }); - } - - }, function (err) { - $scope.success = false; - $scope.error = err; - }); - - $scope.restore = function () { - // this code was copied from `content.move.controller.js` - contentResource.move({ parentId: $scope.target.id, id: node.id }) - .then(function (path) { - - $scope.success = true; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "content", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - }); - }; - }); -function startUpVideosDashboardController($scope, xmlhelper, $log, $http) { - $scope.videos = []; - $scope.init = function(url){ - var proxyUrl = "dashboard/feedproxy.aspx?url=" + url; - $http.get(proxyUrl).then(function(data){ - var feed = $(data.data); - $('item', feed).each(function (i, item) { - var video = {}; - video.thumbnail = $(item).find('thumbnail').attr('url'); - video.title = $("title", item).text(); - video.link = $("guid", item).text(); - $scope.videos.push(video); - }); - }); - }; -} - -angular.module("umbraco").controller("Umbraco.Dashboard.StartupVideosController", startUpVideosDashboardController); - - -function startUpDynamicContentController(dashboardResource, assetsService) { - var vm = this; - vm.loading = true; - vm.showDefault = false; - - // default dashboard content - vm.defaultDashboard = { - infoBoxes: [ - { - title: "Documentation", - description: "Find the answers to your Umbraco questions", - url: "https://our.umbraco.org/documentation/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=documentation/" - }, - { - title: "Community", - description: "Find the answers or ask your Umbraco questions", - url: "https://our.umbraco.org/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=our_forum" - }, - { - title: "Umbraco.tv", - description: "Tutorial videos (some are free, some are on subscription)", - url: "https://umbraco.tv/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=tutorial_videos" - }, - { - title: "Training", - description: "Real-life training and official Umbraco certifications", - url: "https://umbraco.com/training/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=training" - } - ], - articles: [ - { - title: "Umbraco.TV - Learn from the source!", - description: "Umbraco.TV will help you go from zero to Umbraco hero at a pace that suits you. Our easy to follow online training videos will give you the fundamental knowledge to start building awesome Umbraco websites.", - img: "views/dashboard/default/umbracotv.jpg", - url: "https://umbraco.tv/?utm_source=core&utm_medium=dashboard&utm_content=image&utm_campaign=tv", - altText: "Umbraco.TV - Hours of Umbraco Video Tutorials", - buttonText: "Visit Umbraco.TV" - }, - { - title: "Our Umbraco - The Friendliest Community", - description: "Our Umbraco - the official community site is your one stop for everything Umbraco. Whether you need a question answered or looking for cool plugins, the world's best and friendliest community is just a click away.", - img: "views/dashboard/default/ourumbraco.jpg", - url: "https://our.umbraco.org/?utm_source=core&utm_medium=dashboard&utm_content=image&utm_campaign=our", - altText: "Our Umbraco", - buttonText: "Visit Our Umbraco" - } - ] - }; - - - //proxy remote css through the local server - assetsService.loadCss( dashboardResource.getRemoteDashboardCssUrl("content") ); - dashboardResource.getRemoteDashboardContent("content").then( - function (data) { - - vm.loading = false; - - //test if we have received valid data - //we capture it like this, so we avoid UI errors - which automatically triggers ui based on http response code - if (data && data.sections) { - vm.dashboard = data; - } else{ - vm.showDefault = true; - } - - }, - - function (exception) { - console.error(exception); - vm.loading = false; - vm.showDefault = true; - }); -} - -angular.module("umbraco").controller("Umbraco.Dashboard.StartUpDynamicContentController", startUpDynamicContentController); - - -function FormsController($scope, $route, $cookieStore, packageResource, localizationService) { - $scope.installForms = function(){ - $scope.state = localizationService.localize("packager_installStateDownloading"); - packageResource - .fetch("CD44CF39-3D71-4C19-B6EE-948E1FAF0525") - .then(function(pack) { - $scope.state = localizationService.localize("packager_installStateImporting"); - return packageResource.import(pack); - }, - $scope.error) - .then(function(pack) { - $scope.state = localizationService.localize("packager_installStateInstalling"); - return packageResource.installFiles(pack); - }, - $scope.error) - .then(function(pack) { - $scope.state = localizationService.localize("packager_installStateRestarting"); - return packageResource.installData(pack); - }, - $scope.error) - .then(function(pack) { - $scope.state = localizationService.localize("packager_installStateComplete"); - return packageResource.cleanUp(pack); - }, - $scope.error) - .then($scope.complete, $scope.error); - }; - - $scope.complete = function(result){ - var url = window.location.href + "?init=true"; - $cookieStore.put("umbPackageInstallId", result.packageGuid); - window.location.reload(true); - }; - - $scope.error = function(err){ - $scope.state = undefined; - $scope.error = err; - //This will return a rejection meaning that the promise change above will stop - return $q.reject(); - }; - - - function Video_player (videoId) { - // Get dom elements - this.container = document.getElementById(videoId); - this.video = this.container.getElementsByTagName('video')[0]; - - //Create controls - this.controls = document.createElement('div'); - this.controls.className="video-controls"; - - this.seek_bar = document.createElement('input'); - this.seek_bar.className="seek-bar"; - this.seek_bar.type="range"; - this.seek_bar.setAttribute('value', '0'); - - this.loader = document.createElement('div'); - this.loader.className="loader"; - - this.progress_bar = document.createElement('span'); - this.progress_bar.className="progress-bar"; - - // Insert controls - this.controls.appendChild(this.seek_bar); - this.container.appendChild(this.controls); - this.controls.appendChild(this.loader); - this.loader.appendChild(this.progress_bar); - } - - - Video_player.prototype - .seeking = function() { - // get the value of the seekbar (hidden input[type="range"]) - var time = this.video.duration * (this.seek_bar.value / 100); - - // Update video to seekbar value - this.video.currentTime = time; - }; - - // Stop video when user initiates seeking - Video_player.prototype - .start_seek = function() { - this.video.pause(); - }; - - // Start video when user stops seeking - Video_player.prototype - .stop_seek = function() { - this.video.play(); - }; - - // Update the progressbar (span.loader) according to video.currentTime - Video_player.prototype - .update_progress_bar = function() { - // Get video progress in % - var value = (100 / this.video.duration) * this.video.currentTime; - - // Update progressbar - this.progress_bar.style.width = value + '%'; - }; - - // Bind progressbar to mouse when seeking - Video_player.prototype - .handle_mouse_move = function(event) { - // Get position of progressbar relative to browser window - var pos = this.progress_bar.getBoundingClientRect().left; - - // Make sure event is reckonized cross-browser - event = event || window.event; - - // Update progressbar - this.progress_bar.style.width = (event.clientX - pos) + "px"; - }; - - // Eventlisteners for seeking - Video_player.prototype - .video_event_handler = function(videoPlayer, interval) { - // Update the progress bar - var animate_progress_bar = setInterval(function () { - videoPlayer.update_progress_bar(); - }, interval); - - // Fire when input value changes (user seeking) - videoPlayer.seek_bar - .addEventListener("change", function() { - videoPlayer.seeking(); - }); - - // Fire when user clicks on seekbar - videoPlayer.seek_bar - .addEventListener("mousedown", function (clickEvent) { - // Pause video playback - videoPlayer.start_seek(); - - // Stop updating progressbar according to video progress - clearInterval(animate_progress_bar); - - // Update progressbar to where user clicks - videoPlayer.handle_mouse_move(clickEvent); - - // Bind progressbar to cursor - window.onmousemove = function(moveEvent){ - videoPlayer.handle_mouse_move(moveEvent); - }; - }); - - // Fire when user releases seekbar - videoPlayer.seek_bar - .addEventListener("mouseup", function () { - - // Unbind progressbar from cursor - window.onmousemove = null; - - // Start video playback - videoPlayer.stop_seek(); - - // Animate the progressbar - animate_progress_bar = setInterval(function () { - videoPlayer.update_progress_bar(); - }, interval); - }); - }; - - - var videoPlayer = new Video_player('video_1'); - videoPlayer.video_event_handler(videoPlayer, 17); -} - -angular.module("umbraco").controller("Umbraco.Dashboard.FormsDashboardController", FormsController); - -function startupLatestEditsController($scope) { - -} -angular.module("umbraco").controller("Umbraco.Dashboard.StartupLatestEditsController", startupLatestEditsController); - -function MediaFolderBrowserDashboardController($rootScope, $scope, $location, contentTypeResource, userService) { - - var currentUser = {}; - - userService.getCurrentUser().then(function (user) { - - currentUser = user; - - // check if the user start node is the dashboard - if(currentUser.startMediaId === -1) { - - //get the system media listview - contentTypeResource.getPropertyTypeScaffold(-96) - .then(function(dt) { - - $scope.fakeProperty = { - alias: "contents", - config: dt.config, - description: "", - editor: dt.editor, - hideLabel: true, - id: 1, - label: "Contents:", - validation: { - mandatory: false, - pattern: null - }, - value: "", - view: dt.view - }; - - }); - - } else { - // redirect to start node - $location.path("/media/media/edit/" + currentUser.startMediaId); - } - - }); - -} -angular.module("umbraco").controller("Umbraco.Dashboard.MediaFolderBrowserDashboardController", MediaFolderBrowserDashboardController); - -function ExamineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeout) { - - $scope.indexerDetails = []; - $scope.searcherDetails = []; - $scope.loading = true; - - function checkProcessing(indexer, checkActionName) { - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - checkActionName, - { indexerName: indexer.name })), - 'Failed to check index processing') - .then(function(data) { - - if (data !== null && data !== "null") { - - //copy all resulting properties - for (var k in data) { - indexer[k] = data[k]; + function changeValidationType(selectedValidationType) { + if (selectedValidationType) { + $scope.model.property.validation.pattern = selectedValidationType.pattern; + vm.showValidationPattern = true; + // set focus on textarea + if (selectedValidationType.key === 'custom') { + vm.focusOnPatternField = true; } - indexer.isProcessing = false; } else { - $timeout(function() { - //don't continue if we've tried 100 times - if (indexer.processingAttempts < 100) { - checkProcessing(indexer, checkActionName); - //add an attempt - indexer.processingAttempts++; - } else { - //we've exceeded 100 attempts, stop processing - indexer.isProcessing = false; - } - }, - 1000); + $scope.model.property.validation.pattern = ''; + vm.showValidationPattern = false; } - }); - } - - $scope.search = function(searcher, e) { - if (e && e.keyCode !== 13) { - return; + } + activate(); } - - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - "GetSearchResults", - { - searcherName: searcher.name, - query: encodeURIComponent(searcher.searchText), - queryType: searcher.searchType - })), - 'Failed to search') - .then(function(searchResults) { - searcher.isSearching = true; - searcher.searchResults = searchResults; + angular.module('umbraco').controller('Umbraco.Overlay.PropertySettingsOverlay', PropertySettingsOverlay); + }()); + (function () { + 'use strict'; + function CopyOverlay($scope, localizationService, eventsService, entityHelper) { + var vm = this; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('general_copy'); + } + vm.hideSearch = hideSearch; + vm.selectResult = selectResult; + vm.onSearchResults = onSearchResults; + var dialogOptions = $scope.model; + var searchText = 'Search...'; + var node = dialogOptions.currentNode; + localizationService.localize('general_search').then(function (value) { + searchText = value + '...'; }); - } - - $scope.toggle = function(provider, propName) { - if (provider[propName] !== undefined) { - provider[propName] = !provider[propName]; - } else { - provider[propName] = true; - } - } - - $scope.rebuildIndex = function(indexer) { - if (confirm("This will cause the index to be rebuilt. " + - "Depending on how much content there is in your site this could take a while. " + - "It is not recommended to rebuild an index during times of high website traffic " + - "or when editors are editing content.")) { - - indexer.isProcessing = true; - indexer.processingAttempts = 0; - - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - "PostRebuildIndex", - { indexerName: indexer.name })), - 'Failed to rebuild index') - .then(function() { - - //rebuilding has started, nothing is returned accept a 200 status code. - //lets poll to see if it is done. - $timeout(function() { - checkProcessing(indexer, "PostCheckRebuildIndex"); - }, - 1000); - - }); - } - } - - $scope.optimizeIndex = function(indexer) { - if (confirm("This will cause the index to be optimized which will improve its performance. " + - "It is not recommended to optimize an index during times of high website traffic " + - "or when editors are editing content.")) { - indexer.isProcessing = true; - - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - "PostOptimizeIndex", - { indexerName: indexer.name })), - 'Failed to optimize index') - .then(function() { - - //optimizing has started, nothing is returned accept a 200 status code. - //lets poll to see if it is done. - $timeout(function() { - checkProcessing(indexer, "PostCheckOptimizeIndex"); - }, - 1000); - - }); - } - } - - $scope.closeSearch = function(searcher) { - searcher.isSearching = true; - } - - //go get the data - - //combine two promises and execute when they are both done - $q.all([ - - //get the indexer details - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetIndexerDetails")), - 'Failed to retrieve indexer details') - .then(function(data) { - $scope.indexerDetails = data; - }), - - //get the searcher details - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetSearcherDetails")), - 'Failed to retrieve searcher details') - .then(function(data) { - $scope.searcherDetails = data; - for (var s in $scope.searcherDetails) { - $scope.searcherDetails[s].searchType = "text"; + $scope.model.relateToOriginal = true; + $scope.dialogTreeEventHandler = $({}); + vm.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + // get entity type based on the section + $scope.entityType = entityHelper.getEntityTypeFromSection(dialogOptions.section); + function nodeSelectHandler(ev, args) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); } - }) - ]) - .then(function() { - //all init loading is complete - $scope.loading = false; - }); -} - -angular.module("umbraco").controller("Umbraco.Dashboard.ExamineMgmtController", ExamineMgmtController); -(function() { - "use strict"; - - function HealthCheckController($scope, healthCheckResource) { - var SUCCESS = 0; - var WARNING = 1; - var ERROR = 2; - var INFO = 3; - - var vm = this; - - vm.viewState = "list"; - vm.groups = []; - vm.selectedGroup = {}; - - vm.getStatus = getStatus; - vm.executeAction = executeAction; - vm.checkAllGroups = checkAllGroups; - vm.checkAllInGroup = checkAllInGroup; - vm.openGroup = openGroup; - vm.setViewState = setViewState; - - // Get a (grouped) list of all health checks - healthCheckResource.getAllChecks() - .then(function(response) { - vm.groups = response; - }); - - function setGroupGlobalResultType(group) { - var totalSuccess = 0; - var totalError = 0; - var totalWarning = 0; - var totalInfo = 0; - - // count total number of statusses - angular.forEach(group.checks, - function(check) { - angular.forEach(check.status, - function(status) { - switch (status.resultType) { - case SUCCESS: - totalSuccess = totalSuccess + 1; - break; - case WARNING: - totalWarning = totalWarning + 1; - break; - case ERROR: - totalError = totalError + 1; - break; - case INFO: - totalInfo = totalInfo + 1; - break; - } - }); - }); - - group.totalSuccess = totalSuccess; - group.totalError = totalError; - group.totalWarning = totalWarning; - group.totalInfo = totalInfo; - - } - - // Get the status of an individual check - function getStatus(check) { - check.loading = true; - check.status = null; - healthCheckResource.getStatus(check.id) - .then(function(response) { - check.loading = false; - check.status = response; - }); - } - - function executeAction(check, index, action) { - check.loading = true; - healthCheckResource.executeAction(action) - .then(function(response) { - check.status[index] = response; - check.loading = false; - }); - } - - function checkAllGroups(groups) { - // set number of checks which has been executed - for (var i = 0; i < groups.length; i++) { - var group = groups[i]; - checkAllInGroup(group, group.checks); + //eventsService.emit("editors.content.copyController.select", args); + if ($scope.model.target) { + //un-select if there's a current one selected + $scope.model.target.selected = false; + } + $scope.model.target = args.node; + $scope.model.target.selected = true; } - vm.groups = groups; - } - - function checkAllInGroup(group, checks) { - group.checkCounter = 0; - group.loading = true; - - angular.forEach(checks, - function(check) { - - check.loading = true; - - healthCheckResource.getStatus(check.id) - .then(function(response) { - check.status = response; - group.checkCounter = group.checkCounter + 1; - check.loading = false; - - // when all checks are done, set global group result - if (group.checkCounter === checks.length) { - setGroupGlobalResultType(group); - group.loading = false; - } - }); - }); - } - - function openGroup(group) { - vm.selectedGroup = group; - vm.viewState = "details"; - } - - function setViewState(state) { - vm.viewState = state; - - if (state === 'list') { - - for (var i = 0; i < vm.groups.length; i++) { - var group = vm.groups[i]; - setGroupGlobalResultType(group); + function nodeExpandedHandler(ev, args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); } } - } - } - - angular.module("umbraco").controller("Umbraco.Dashboard.HealthCheckController", HealthCheckController); -})(); - -(function() { - "use strict"; - - function RedirectUrlsController($scope, redirectUrlsResource, notificationsService, localizationService, $q) { - //...todo - //search by url or url part - //search by domain - //display domain in dashboard results? - - //used to cancel any request in progress if another one needs to take it's place - var vm = this; - var canceler = null; - - vm.dashboard = { - searchTerm: "", - loading: false, - urlTrackerDisabled: false, - userIsAdmin: false - }; - - vm.pagination = { - pageIndex: 0, - pageNumber: 1, - totalPages: 1, - pageSize: 20 - }; - - vm.goToPage = goToPage; - vm.search = search; - vm.removeRedirect = removeRedirect; - vm.disableUrlTracker = disableUrlTracker; - vm.enableUrlTracker = enableUrlTracker; - vm.filter = filter; - vm.checkEnabled = checkEnabled; - - function activate() { - vm.checkEnabled().then(function() { - vm.search(); - }); - } - - function checkEnabled() { - vm.dashboard.loading = true; - return redirectUrlsResource.getEnableState().then(function (response) { - vm.dashboard.urlTrackerDisabled = response.enabled !== true; - vm.dashboard.userIsAdmin = response.userIsAdmin; - vm.dashboard.loading = false; - }); - } - - function goToPage(pageNumber) { - vm.pagination.pageIndex = pageNumber - 1; - vm.pagination.pageNumber = pageNumber; - vm.search(); - } - - function search() { - - vm.dashboard.loading = true; - - var searchTerm = vm.dashboard.searchTerm; - if (searchTerm === undefined) { - searchTerm = ""; - } - - redirectUrlsResource.searchRedirectUrls(searchTerm, vm.pagination.pageIndex, vm.pagination.pageSize).then(function(response) { - - vm.redirectUrls = response.searchResults; - - // update pagination - vm.pagination.pageIndex = response.currentPage; - vm.pagination.pageNumber = response.currentPage + 1; - vm.pagination.totalPages = response.pageCount; - - vm.dashboard.loading = false; - + function hideSearch() { + vm.searchInfo.showSearch = false; + vm.searchInfo.searchFromId = null; + vm.searchInfo.searchFromName = null; + vm.searchInfo.results = []; + } + // method to select a search result + function selectResult(evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { + event: evt, + node: result + }); + } + //callback when there are search results + function onSearchResults(results) { + vm.searchInfo.results = results; + vm.searchInfo.showSearch = true; + } + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + function openMiniListView(node) { + $scope.miniListView = node; + } } - - function removeRedirect(redirectToDelete) { - localizationService.localize("redirectUrls_confirmRemove", [redirectToDelete.originalUrl, redirectToDelete.destinationUrl]).then(function (value) { - var toggleConfirm = confirm(value); - - if (toggleConfirm) { - redirectUrlsResource.deleteRedirectUrl(redirectToDelete.redirectId).then(function () { - - var index = vm.redirectUrls.indexOf(redirectToDelete); - vm.redirectUrls.splice(index, 1); - notificationsService.success(localizationService.localize("redirectUrls_redirectRemoved")); - - // check if new redirects needs to be loaded - if (vm.redirectUrls.length === 0 && vm.pagination.totalPages > 1) { - - // if we are not on the first page - get records from the previous - if (vm.pagination.pageIndex > 0) { - vm.pagination.pageIndex = vm.pagination.pageIndex - 1; - vm.pagination.pageNumber = vm.pagination.pageNumber - 1; - } - - search(); + angular.module('umbraco').controller('Umbraco.Overlays.CopyOverlay', CopyOverlay); + }()); + (function () { + 'use strict'; + function EmbedOverlay($scope, $http, umbRequestHelper, localizationService) { + var vm = this; + var origWidth = 500; + var origHeight = 300; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('general_embed'); + } + $scope.model.embed = { + url: '', + width: 360, + height: 240, + constrain: true, + preview: '', + success: false, + info: '', + supportsDimensions: '' + }; + vm.showPreview = showPreview; + vm.changeSize = changeSize; + function showPreview() { + if ($scope.model.embed.url) { + $scope.model.embed.show = true; + $scope.model.embed.preview = '
'; + $scope.model.embed.info = ''; + $scope.model.embed.success = false; + $http({ + method: 'GET', + url: umbRequestHelper.getApiUrl('embedApiBaseUrl', 'GetEmbed'), + params: { + url: $scope.model.embed.url, + width: $scope.model.embed.width, + height: $scope.model.embed.height + } + }).success(function (data) { + $scope.model.embed.preview = ''; + switch (data.Status) { + case 0: + //not supported + $scope.model.embed.info = 'Not supported'; + break; + case 1: + //error + $scope.model.embed.info = 'Could not embed media - please ensure the URL is valid'; + break; + case 2: + $scope.model.embed.preview = data.Markup; + $scope.model.embed.supportsDimensions = data.SupportsDimensions; + $scope.model.embed.success = true; + break; } - }, function (error) { - notificationsService.error(localizationService.localize("redirectUrls_redirectRemoveError")); + }).error(function () { + $scope.model.embed.supportsDimensions = false; + $scope.model.embed.preview = ''; + $scope.model.embed.info = 'Could not embed media - please ensure the URL is valid'; }); + } else { + $scope.model.embed.supportsDimensions = false; + $scope.model.embed.preview = ''; + $scope.model.embed.info = 'Please enter a URL'; } - }); - } - - function disableUrlTracker() { - localizationService.localize("redirectUrls_confirmDisable").then(function(value) { - var toggleConfirm = confirm(value); - if (toggleConfirm) { - - redirectUrlsResource.toggleUrlTracker(true).then(function () { - activate(); - notificationsService.success(localizationService.localize("redirectUrls_disabledConfirm")); - }, function (error) { - notificationsService.warning(localizationService.localize("redirectUrls_disableError")); - }); - + } + function changeSize(type) { + var width, height; + if ($scope.model.embed.constrain) { + width = parseInt($scope.model.embed.width, 10); + height = parseInt($scope.model.embed.height, 10); + if (type == 'width') { + origHeight = Math.round(width / origWidth * height); + $scope.model.embed.height = origHeight; + } else { + origWidth = Math.round(height / origHeight * width); + $scope.model.embed.width = origWidth; + } } - }); + if ($scope.model.embed.url !== '') { + showPreview(); + } + } } - - function enableUrlTracker() { - redirectUrlsResource.toggleUrlTracker(false).then(function() { - activate(); - notificationsService.success(localizationService.localize("redirectUrls_enabledConfirm")); - }, function(error) { - notificationsService.warning(localizationService.localize("redirectUrls_enableError")); - }); + angular.module('umbraco').controller('Umbraco.Overlays.EmbedOverlay', EmbedOverlay); + }()); + angular.module('umbraco').controller('Umbraco.Overlays.HelpController', function ($scope, $location, $routeParams, helpService, userService, localizationService) { + $scope.section = $routeParams.section; + $scope.version = Umbraco.Sys.ServerVariables.application.version + ' assembly: ' + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.model.subtitle = 'Umbraco version' + ' ' + $scope.version; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('general_help'); } - - var filterDebounced = _.debounce(function(e) { - - $scope.$apply(function() { - - //a canceler exists, so perform the cancelation operation and reset - if (canceler) { - canceler.resolve(); - canceler = $q.defer(); - } else { - canceler = $q.defer(); - } - - vm.search(); - - }); - - }, 200); - - function filter() { - vm.dashboard.loading = true; - filterDebounced(); + if (!$scope.section) { + $scope.section = 'content'; } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.Dashboard.RedirectUrlsController", RedirectUrlsController); -})(); - -function XmlDataIntegrityReportController($scope, umbRequestHelper, $log, $http) { - - function check(item) { - var action = item.check; - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("xmlDataIntegrityBaseUrl", action)), - 'Failed to retrieve data integrity status') - .then(function(result) { - item.checking = false; - item.invalid = result === "false"; - }); - } - - $scope.fix = function(item) { - var action = item.fix; - if (item.fix) { - if (confirm("This will cause all xml structures for this type to be rebuilt. " + - "Depending on how much content there is in your site this could take a while. " + - "It is not recommended to rebuild xml structures if they are not out of sync, during times of high website traffic " + - "or when editors are editing content.")) { - item.fixing = true; - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("xmlDataIntegrityBaseUrl", action)), - 'Failed to retrieve data integrity status') - .then(function(result) { - item.fixing = false; - item.invalid = result === "false"; - }); + $scope.sectionName = $scope.section; + var rq = {}; + rq.section = $scope.section; + //translate section name + localizationService.localize('sections_' + rq.section).then(function (value) { + $scope.sectionName = value; + }); + userService.getCurrentUser().then(function (user) { + rq.lang = user.locale; + if ($routeParams.url) { + rq.path = decodeURIComponent($routeParams.url); + if (rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0) { + rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); + } + if (rq.path.indexOf('.aspx') > 0) { + rq.path = rq.path.substring(0, rq.path.indexOf('.aspx')); + } + } else { + rq.path = rq.section + '/' + $routeParams.tree + '/' + $routeParams.method; } - } - } - - $scope.items = { - "contentXml": { - label: "Content in the cmsContentXml table", - checking: true, - fixing: false, - fix: "FixContentXmlTable", - check: "CheckContentXmlTable" - }, - "mediaXml": { - label: "Media in the cmsContentXml table", - checking: true, - fixing: false, - fix: "FixMediaXmlTable", - check: "CheckMediaXmlTable" - }, - "memberXml": { - label: "Members in the cmsContentXml table", - checking: true, - fixing: false, - fix: "FixMembersXmlTable", - check: "CheckMembersXmlTable" - } - }; - - for (var i in $scope.items) { - check($scope.items[i]); - } -} - -angular.module("umbraco").controller("Umbraco.Dashboard.XmlDataIntegrityReportController", XmlDataIntegrityReportController); -/** - * @ngdoc controller - * @name Umbraco.Editors.DataType.CreateController - * @function - * - * @description - * The controller for the data type creation dialog - */ -function DataTypeCreateController($scope, $location, navigationService, dataTypeResource, formHelper, appState) { - - $scope.model = { - folderName: "", - creatingFolder: false - }; - - var node = $scope.dialogOptions.currentNode; - - $scope.showCreateFolder = function() { - $scope.model.creatingFolder = true; - } - - $scope.createContainer = function () { - if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm, statusMessage: "Creating folder..." })) { - dataTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { - - navigationService.hideMenu(); - var currPath = node.path ? node.path : "-1"; - navigationService.syncTree({ tree: "datatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - - formHelper.resetForm({ scope: $scope }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - //TODO: Handle errors + helpService.findHelp(rq).then(function (topics) { + $scope.topics = topics; + }); + helpService.findVideos(rq).then(function (videos) { + $scope.videos = videos; }); - }; - } - - $scope.createDataType = function() { - $location.search('create', null); - $location.path("/developer/datatypes/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - } -} - -angular.module('umbraco').controller("Umbraco.Editors.DataType.CreateController", DataTypeCreateController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.ContentDeleteController - * @function - * - * @description - * The controller for deleting content - */ -function DataTypeDeleteController($scope, dataTypeResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - dataTypeResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.performContainerDelete = function () { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - dataTypeResource.deleteContainerById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.DataType.DeleteController", DataTypeDeleteController); - -/** + }); + /** * @ngdoc controller - * @name Umbraco.Editors.DataType.EditController + * @name Umbraco.Editors.DocumentType.PropertyController * @function * * @description - * The controller for the content editor - */ -function DataTypeEditController($scope, $routeParams, $location, appState, navigationService, treeService, dataTypeResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, formHelper, editorState, dataTypeHelper, eventsService) { - - //setup scope vars - $scope.page = {}; - $scope.page.loading = false; - $scope.page.nameLocked = false; - $scope.page.menu = {}; - $scope.page.menu.currentSection = appState.getSectionState("currentSection"); - $scope.page.menu.currentNode = null; - var evts = []; - - //method used to configure the pre-values when we retrieve them from the server - function createPreValueProps(preVals) { - $scope.preValues = []; - for (var i = 0; i < preVals.length; i++) { - $scope.preValues.push({ - hideLabel: preVals[i].hideLabel, - alias: preVals[i].key, - description: preVals[i].description, - label: preVals[i].label, - view: preVals[i].view, - value: preVals[i].value, - config: preVals[i].config, - }); - } - } - - //set up the standard data type props - $scope.properties = { - selectedEditor: { - alias: "selectedEditor", - description: "Select a property editor", - label: "Property editor" - }, - selectedEditorId: { - alias: "selectedEditorId", - label: "Property editor alias" - } - }; - - //setup the pre-values as props - $scope.preValues = []; - - if ($routeParams.create) { - - $scope.page.loading = true; - - //we are creating so get an empty data type item - dataTypeResource.getScaffold($routeParams.id) - .then(function(data) { - - $scope.preValuesLoaded = true; - $scope.content = data; - - setHeaderNameState($scope.content); - - //set a shared state - editorState.set($scope.content); - - $scope.page.loading = false; - - }); - } - else { - loadDataType(); - } - - function loadDataType() { - - $scope.page.loading = true; - - //we are editing so get the content item from the server - dataTypeResource.getById($routeParams.id) - .then(function(data) { - - $scope.preValuesLoaded = true; - $scope.content = data; - - createPreValueProps($scope.content.preValues); - - setHeaderNameState($scope.content); - - //share state - editorState.set($scope.content); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - navigationService.syncTree({ tree: "datatypes", path: data.path }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; - }); - - $scope.page.loading = false; - - }); - } - - $scope.$watch("content.selectedEditor", function (newVal, oldVal) { - - //when the value changes, we need to dynamically load in the new editor - if (newVal && (newVal != oldVal && (oldVal || $routeParams.create))) { - //we are editing so get the content item from the server - var currDataTypeId = $routeParams.create ? undefined : $routeParams.id; - dataTypeResource.getPreValues(newVal, currDataTypeId) - .then(function (data) { - $scope.preValuesLoaded = true; - $scope.content.preValues = data; - createPreValueProps($scope.content.preValues); - - setHeaderNameState($scope.content); - - //share state - editorState.set($scope.content); - }); - } - }); - - function setHeaderNameState(content) { - - if(content.isSystem == 1) { - $scope.page.nameLocked = true; - } - - } - - $scope.save = function() { - - if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { - - $scope.page.saveButtonState = "busy"; - - dataTypeResource.save($scope.content, $scope.preValues, $routeParams.create) - .then(function(data) { - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: function() { - createPreValueProps(data.preValues); - } - }); - - setHeaderNameState($scope.content); - - //share state - editorState.set($scope.content); - - navigationService.syncTree({ tree: "datatypes", path: data.path, forceReload: true }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; - }); - - $scope.page.saveButtonState = "success"; - - dataTypeHelper.rebindChangedProperties($scope.content, data); - - }, function(err) { - - //NOTE: in the case of data type values we are setting the orig/new props - // to be the same thing since that only really matters for content/media. - contentEditingHelper.handleSaveError({ - redirectOnFailure: false, - err: err - }); - - $scope.page.saveButtonState = "error"; - - //share state - editorState.set($scope.content); - }); - } - - }; - - evts.push(eventsService.on("app.refreshEditor", function(name, error) { - loadDataType(); - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); - -} - -angular.module("umbraco").controller("Umbraco.Editors.DataType.EditController", DataTypeEditController); - -angular.module("umbraco") -.controller("Umbraco.Editors.DataType.MoveController", - function ($scope, dataTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; + * The controller for the content type editor property dialog + */ + function IconPickerOverlay($scope, iconHelper, localizationService) { + $scope.loading = true; + $scope.model.hideSubmitButton = true; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectIcon'); } - - $scope.move = function () { - - $scope.busy = true; - $scope.error = false; - - dataTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "dataTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "dataTypes", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - eventsService.emit('app.refreshEditor'); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); + iconHelper.getIcons().then(function (icons) { + $scope.icons = icons; + $scope.loading = false; + }); + $scope.selectIcon = function (icon, color) { + $scope.model.icon = icon; + $scope.model.color = color; + $scope.submitForm($scope.model); + }; + } + angular.module('umbraco').controller('Umbraco.Overlays.IconPickerOverlay', IconPickerOverlay); + (function () { + 'use strict'; + function InsertOverlayController($scope, localizationService) { + var vm = this; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('template_insert'); + } + if (!$scope.model.subtitle) { + $scope.model.subtitle = localizationService.localize('template_insertDesc'); + } + vm.openMacroPicker = openMacroPicker; + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openPartialOverlay = openPartialOverlay; + function openMacroPicker() { + vm.macroPickerOverlay = { + view: 'macropicker', + title: localizationService.localize('template_insertMacro'), + dialogData: {}, + show: true, + submit: function (model) { + $scope.model.insert = { + 'type': 'macro', + 'macroParams': model.macroParams, + 'selectedMacro': model.selectedMacro + }; + $scope.model.submit($scope.model); + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + } + }; + } + function openPageFieldOverlay() { + vm.pageFieldOverlay = { + title: localizationService.localize('template_insertPageField'), + description: localizationService.localize('template_insertPageFieldDesc'), + submitButtonLabel: 'Insert', + closeButtonlabel: 'Cancel', + view: 'insertfield', + show: true, + submit: function (model) { + $scope.model.insert = { + 'type': 'umbracoField', + 'umbracoField': model.umbracoField + }; + $scope.model.submit($scope.model); + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + }, + close: function (model) { + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + } + }; + } + function openDictionaryItemOverlay() { + vm.dictionaryItemOverlay = { + view: 'treepicker', + section: 'settings', + treeAlias: 'dictionary', + entityType: 'dictionary', + multiPicker: false, + title: localizationService.localize('template_insertDictionaryItem'), + description: localizationService.localize('template_insertDictionaryItemDesc'), + emptyStateMessage: localizationService.localize('emptyStates_emptyDictionaryTree'), + show: true, + select: function (node) { + $scope.model.insert = { + 'type': 'dictionary', + 'node': node + }; + $scope.model.submit($scope.model); + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + }, + close: function (model) { + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + } + }; + } + function openPartialOverlay() { + vm.partialItemOverlay = { + view: 'treepicker', + section: 'settings', + treeAlias: 'partialViews', + entityType: 'partialView', + multiPicker: false, + filter: function (i) { + if (i.name.indexOf('.cshtml') === -1 && i.name.indexOf('.vbhtml') === -1) { + return true; } + }, + filterCssClass: 'not-allowed', + title: localizationService.localize('template_insertPartialView'), + show: true, + select: function (node) { + $scope.model.insert = { + 'type': 'partial', + 'node': node + }; + $scope.model.submit($scope.model); + vm.partialItemOverlay.show = false; + vm.partialItemOverlay = null; + }, + close: function (model) { + vm.partialItemOverlay.show = false; + vm.partialItemOverlay = null; } + }; + } + } + angular.module('umbraco').controller('Umbraco.Overlays.InsertOverlay', InsertOverlayController); + }()); + (function () { + 'use strict'; + function InsertFieldController($scope, $http, contentTypeResource) { + var vm = this; + vm.field; + vm.altField; + vm.altText; + vm.insertBefore; + vm.insertAfter; + vm.recursive = false; + vm.properties = []; + vm.standardFields = []; + vm.date = false; + vm.dateTime = false; + vm.dateTimeSeparator = ''; + vm.casingUpper = false; + vm.casingLower = false; + vm.encodeHtml = false; + vm.encodeUrl = false; + vm.convertLinebreaks = false; + vm.showAltField = false; + vm.showAltText = false; + vm.setDateOption = setDateOption; + vm.setCasingOption = setCasingOption; + vm.setEncodingOption = setEncodingOption; + vm.generateOutputSample = generateOutputSample; + function onInit() { + // set default title + if (!$scope.model.title) { + $scope.model.title = 'Insert value'; + } + // Load all fields + contentTypeResource.getAllPropertyTypeAliases().then(function (array) { + vm.properties = array; }); + // Load all standard fields + contentTypeResource.getAllStandardFields().then(function (array) { + vm.standardFields = array; + }); + } + // date formatting + function setDateOption(option) { + if (option === 'date') { + if (vm.date) { + vm.date = false; + } else { + vm.date = true; + vm.dateTime = false; + } + } + if (option === 'dateWithTime') { + if (vm.dateTime) { + vm.dateTime = false; + } else { + vm.date = false; + vm.dateTime = true; + } + } + } + // casing formatting + function setCasingOption(option) { + if (option === 'uppercase') { + if (vm.casingUpper) { + vm.casingUpper = false; + } else { + vm.casingUpper = true; + vm.casingLower = false; + } + } + if (option === 'lowercase') { + if (vm.casingLower) { + vm.casingLower = false; + } else { + vm.casingUpper = false; + vm.casingLower = true; + } + } + } + // encoding formatting + function setEncodingOption(option) { + if (option === 'html') { + if (vm.encodeHtml) { + vm.encodeHtml = false; + } else { + vm.encodeHtml = true; + vm.encodeUrl = false; + } + } + if (option === 'url') { + if (vm.encodeUrl) { + vm.encodeUrl = false; + } else { + vm.encodeHtml = false; + vm.encodeUrl = true; + } + } + } + function generateOutputSample() { + var pageField = (vm.field !== undefined ? '@Umbraco.Field("' + vm.field + '"' : '') + (vm.altField !== undefined ? ', altFieldAlias:"' + vm.altField + '"' : '') + (vm.altText !== undefined ? ', altText:"' + vm.altText + '"' : '') + (vm.insertBefore !== undefined ? ', insertBefore:"' + vm.insertBefore + '"' : '') + (vm.insertAfter !== undefined ? ', insertAfter:"' + vm.insertAfter + '"' : '') + (vm.recursive !== false ? ', recursive: ' + vm.recursive : '') + (vm.date !== false ? ', formatAsDate: ' + vm.date : '') + (vm.dateTime !== false ? ', formatAsDateWithTimeSeparator:"' + vm.dateTimeSeparator + '"' : '') + (vm.casingUpper !== false ? ', casing: ' + 'RenderFieldCaseType.Upper' : '') + (vm.casingLower !== false ? ', casing: ' + 'RenderFieldCaseType.Lower' : '') + (vm.encodeHtml !== false ? ', encoding: ' + 'RenderFieldEncodingType.Html' : '') + (vm.encodeUrl !== false ? ', encoding: ' + 'RenderFieldEncodingType.Url' : '') + (vm.convertLinebreaks !== false ? ', convertLineBreaks: ' + 'true' : '') + (vm.field ? ')' : ''); + $scope.model.umbracoField = pageField; + return pageField; + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Overlays.InsertFieldController', InsertFieldController); + }()); + function ItemPickerOverlay($scope, localizationService) { + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectItem'); + } + $scope.model.hideSubmitButton = true; + $scope.selectItem = function (item) { + $scope.model.selectedItem = item; + $scope.submitForm($scope.model); }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + } + angular.module('umbraco').controller('Umbraco.Overlays.ItemPickerOverlay', ItemPickerOverlay); + //used for the media picker dialog + angular.module('umbraco').controller('Umbraco.Overlays.LinkPickerController', function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { + var dialogOptions = $scope.model; + var searchText = 'Search...'; + localizationService.localize('general_search').then(function (value) { + searchText = value + '...'; }); - }); - -angular.module("umbraco") -.controller("Umbraco.Editors.DocumentTypes.CopyController", - function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - var dialogOptions = $scope.dialogOptions; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectLink'); + } $scope.dialogTreeEventHandler = $({}); - + $scope.model.target = {}; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + if (dialogOptions.currentTarget) { + $scope.model.target = dialogOptions.currentTarget; + //if we have a node ID, we fetch the current node to build the form data + if ($scope.model.target.id || $scope.model.target.udi) { + //will be either a udi or an int + var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; + if (!$scope.model.target.path) { + entityResource.getPath(id, 'Document').then(function (path) { + $scope.model.target.path = path; + //now sync the tree to this path + $scope.dialogTreeEventHandler.syncTree({ + path: $scope.model.target.path, + tree: 'content' + }); + }); + } + contentResource.getNiceUrl(id).then(function (url) { + $scope.model.target.url = url; + }); + } + } function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); + } + eventsService.emit('dialogs.linkPicker.select', args); + if ($scope.currentNode) { //un-select if there's a current one selected - $scope.target.selected = false; + $scope.currentNode.selected = false; + } + $scope.currentNode = args.node; + $scope.currentNode.selected = true; + $scope.model.target.id = args.node.id; + $scope.model.target.udi = args.node.udi; + $scope.model.target.name = args.node.name; + if (args.node.id < 0) { + $scope.model.target.url = '/'; + } else { + contentResource.getNiceUrl(args.node.id).then(function (url) { + $scope.model.target.url = url; + }); + } + if (!angular.isUndefined($scope.model.target.isMedia)) { + delete $scope.model.target.isMedia; } - - $scope.target = args.node; - $scope.target.selected = true; } - - $scope.copy = function () { - - $scope.busy = true; - $scope.error = false; - - contentTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the copied content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was copied!!) - - navigationService.syncTree({ tree: "documentTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "documentTypes", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } + function nodeExpandedHandler(ev, args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } + $scope.switchToMediaPicker = function () { + userService.getCurrentUser().then(function (userData) { + $scope.mediaPickerOverlay = { + view: 'mediapicker', + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], + startNodeIsVirtual: userData.startMediaIds.length !== 1, + show: true, + submit: function (model) { + var media = model.selectedImages[0]; + $scope.model.target.id = media.id; + $scope.model.target.udi = media.udi; + $scope.model.target.isMedia = true; + $scope.model.target.name = media.name; + $scope.model.target.url = mediaHelper.resolveFile(media); + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; } - }); + }; + }); + }; + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + }; + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { + event: evt, + node: result + }); + }; + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + function openMiniListView(node) { + $scope.miniListView = node; + } }); - -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.CreateController - * @function - * - * @description - * The controller for the doc type creation dialog - */ -function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService) { - - $scope.model = { - allowCreateFolder: $scope.dialogOptions.currentNode.parentId === null || $scope.dialogOptions.currentNode.nodeType === "container", - folderName: "", - creatingFolder: false, - }; - - var node = $scope.dialogOptions.currentNode, - localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); - - $scope.showCreateFolder = function() { - $scope.model.creatingFolder = true; - }; - - $scope.createContainer = function() { - - if (formHelper.submitForm({scope: $scope, formCtrl: this.createFolderForm, statusMessage: localizeCreateFolder})) { - - contentTypeResource.createContainer(node.id, $scope.model.folderName).then(function(folderId) { - - navigationService.hideMenu(); - - var currPath = node.path ? node.path : "-1"; - - navigationService.syncTree({ - tree: "documenttypes", - path: currPath + "," + folderId, - forceReload: true, - activate: true - }); - - formHelper.resetForm({ - scope: $scope - }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - $scope.error = err; - - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); + function MacroPickerController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper, localizationService) { + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectMacro'); + } + $scope.macros = []; + $scope.model.selectedMacro = null; + $scope.model.macroParams = []; + $scope.wizardStep = 'macroSelect'; + $scope.noMacroParams = false; + $scope.selectMacro = function (macro) { + $scope.model.selectedMacro = macro; + if ($scope.wizardStep === 'macroSelect') { + editParams(); + } else { + $scope.model.submit($scope.model); + } + }; + /** changes the view to edit the params of the selected macro */ + function editParams() { + //get the macro params if there are any + macroResource.getMacroParameters($scope.model.selectedMacro.id).then(function (data) { + //go to next page if there are params otherwise we can just exit + if (!angular.isArray(data) || data.length === 0) { + $scope.model.submit($scope.model); + } else { + $scope.wizardStep = 'paramSelect'; + $scope.model.macroParams = data; + //fill in the data if we are editing this macro + if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroParamsDictionary) { + _.each($scope.model.dialogData.macroData.macroParamsDictionary, function (val, key) { + var prop = _.find($scope.model.macroParams, function (item) { + return item.alias == key; + }); + if (prop) { + if (_.isString(val)) { + //we need to unescape values as they have most likely been escaped while inserted + val = _.unescape(val); + //detect if it is a json string + if (val.detectIsJson()) { + try { + //Parse it to json + prop.value = angular.fromJson(val); + } catch (e) { + // not json + prop.value = val; + } + } else { + prop.value = val; + } + } else { + prop.value = val; + } + } + }); } } }); } - }; - - $scope.createDocType = function() { - $location.search('create', null); - $location.search('notemplate', null); - $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - }; - - $scope.createComponent = function() { - $location.search('create', null); - $location.search('notemplate', null); - $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true").search("notemplate", "true"); - navigationService.hideMenu(); - }; -} - -angular.module('umbraco').controller("Umbraco.Editors.DocumentTypes.CreateController", DocumentTypesCreateController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.DeleteController - * @function - * - * @description - * The controller for deleting content - */ -function DocumentTypesDeleteController($scope, dataTypeResource, contentTypeResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - contentTypeResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.performContainerDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - contentTypeResource.deleteContainerById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.DeleteController", DocumentTypesDeleteController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.EditController - * @function - * - * @description - * The controller for the content type editor - */ -(function () { - "use strict"; - - function DocumentTypesEditController($scope, $routeParams, $injector, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { - - var vm = this; - var localizeSaving = localizationService.localize("general_saving"); - var evts = []; - - vm.save = save; - - vm.currentNode = null; - vm.contentType = {}; - - vm.page = {}; - vm.page.loading = false; - vm.page.saveButtonState = "init"; - vm.page.navigation = [ - { - "name": localizationService.localize("general_design"), - "icon": "icon-document-dashed-line", - "view": "views/documenttypes/views/design/design.html", - "active": true - }, - { - "name": localizationService.localize("general_listView"), - "icon": "icon-list", - "view": "views/documenttypes/views/listview/listview.html" - }, - { - "name": localizationService.localize("general_rights"), - "icon": "icon-keychain", - "view": "views/documenttypes/views/permissions/permissions.html" - }, - { - "name": localizationService.localize("treeHeaders_templates"), - "icon": "icon-layout", - "view": "views/documenttypes/views/templates/templates.html" - } - ]; - - vm.page.keyboardShortcutsOverview = [ - { - "name": localizationService.localize("main_sections"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_navigateSections"), - "keys": [{ "key": "1" }, { "key": "4" }], - "keyRange": true - } - ] - }, - { - "name": localizationService.localize("general_design"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_addTab"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] - }, - { - "description": localizationService.localize("shortcuts_addProperty"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] - }, - { - "description": localizationService.localize("shortcuts_addEditor"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] - }, - { - "description": localizationService.localize("shortcuts_editDataType"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] - } - ] - }, - { - "name": localizationService.localize("general_listView"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_toggleListView"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] - } - ] - }, - { - "name": localizationService.localize("general_rights"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_toggleAllowAsRoot"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] - }, - { - "description": localizationService.localize("shortcuts_addChildNode"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] - } - ] - }, - { - "name": localizationService.localize("treeHeaders_templates"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_addTemplate"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] - } - ] - } - ]; - - contentTypeHelper.checkModelsBuilderStatus().then(function (result) { - vm.page.modelsBuilder = result; - if (result) { - //Models builder mode: - vm.page.defaultButton = { - hotKey: "ctrl+s", - hotKeyWhenHidden: true, - labelKey: "buttons_save", - letter: "S", - type: "submit", - handler: function () { vm.save(); } - }; - vm.page.subButtons = [{ - hotKey: "ctrl+g", - hotKeyWhenHidden: true, - labelKey: "buttons_saveAndGenerateModels", - letter: "G", - handler: function () { - - vm.page.saveButtonState = "busy"; - - vm.save().then(function (result) { - - vm.page.saveButtonState = "busy"; - - localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { - localizationService.localize("modelsBuilder_waitingMessage").then(function(msgValue) { - notificationsService.info(headerValue, msgValue); - }); - }); - - contentTypeHelper.generateModels().then(function (result) { - - // generateModels() returns the dashboard content - if (!result.lastError) { - - //re-check model status - contentTypeHelper.checkModelsBuilderStatus().then(function(statusResult) { - vm.page.modelsBuilder = statusResult; - }); - - //clear and add success - vm.page.saveButtonState = "init"; - localizationService.localize("modelsBuilder_modelsGenerated").then(function(value) { - notificationsService.success(value); - }); - - } else { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function(value) { - notificationsService.error(value); - }); - } - - }, function () { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsGeneratedError").then(function(value) { - notificationsService.error(value); - }); - }); - - }); - - } - }]; - } - }); - - if ($routeParams.create) { - vm.page.loading = true; - - //we are creating so get an empty data type item - contentTypeResource.getScaffold($routeParams.id) - .then(function (dt) { - - init(dt); - - vm.page.loading = false; - - }); - } - else { - loadDocumentType(); - } - - function loadDocumentType() { - - vm.page.loading = true; - - contentTypeResource.getById($routeParams.id).then(function (dt) { - init(dt); - - syncTreeNode(vm.contentType, dt.path, true); - - vm.page.loading = false; - - }); - - } - - - /* ---------- SAVE ---------- */ - - function save() { - - // only save if there is no overlays open - if(overlayHelper.getNumberOfOverlays() === 0) { - - var deferred = $q.defer(); - - vm.page.saveButtonState = "busy"; - - // reformat allowed content types to array if id's - vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: localizeSaving, - saveMethod: contentTypeResource.save, - scope: $scope, - content: vm.contentType, - //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - // we need to rebind... the IDs that have been created! - rebindCallback: function (origContentType, savedContentType) { - vm.contentType.id = savedContentType.id; - vm.contentType.groups.forEach(function(group) { - if (!group.name) return; - - var k = 0; - while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) - k++; - if (k == savedContentType.groups.length) { - group.id = 0; - return; - } - - var savedGroup = savedContentType.groups[k]; - if (!group.id) group.id = savedGroup.id; - - group.properties.forEach(function (property) { - if (property.id || !property.alias) return; - - k = 0; - while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) - k++; - if (k == savedGroup.properties.length) { - property.id = 0; - return; - } - - var savedProperty = savedGroup.properties[k]; - property.id = savedProperty.id; - }); - }); - } - }).then(function (data) { - //success - syncTreeNode(vm.contentType, data.path); - - vm.page.saveButtonState = "success"; - - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } - else { - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); - } - vm.page.saveButtonState = "error"; - - deferred.reject(err); - }); - return deferred.promise; - - } - - } - - function init(contentType) { - - // set all tab to inactive - if (contentType.groups.length !== 0) { - angular.forEach(contentType.groups, function (group) { - - angular.forEach(group.properties, function (property) { - // get data type details for each property - getDataTypeDetails(property); - }); - - }); - } - - // insert template on new doc types - if (!$routeParams.notemplate && contentType.id === 0) { - contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); - contentType.allowedTemplates = contentTypeHelper.insertTemplatePlaceholder(contentType.allowedTemplates); - } - - // convert icons for content type - convertLegacyIcons(contentType); - - //set a shared state - editorState.set(contentType); - - vm.contentType = contentType; - } - - function convertLegacyIcons(contentType) { - // make array to store contentType icon - var contentTypeArray = []; - - // push icon to array - contentTypeArray.push({ "icon": contentType.icon }); - - // run through icon method - iconHelper.formatContentTypeIcons(contentTypeArray); - - // set icon back on contentType - contentType.icon = contentTypeArray[0].icon; - } - - function getDataTypeDetails(property) { - if (property.propertyState !== "init") { - - dataTypeResource.getById(property.dataTypeId) - .then(function (dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - } - } - - /** Syncs the content type to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(dt, path, initialLoad) { - navigationService.syncTree({ tree: "documenttypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - vm.currentNode = syncArgs.node; - }); - } - - evts.push(eventsService.on("app.refreshEditor", function(name, error) { - loadDocumentType(); - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); - - } - - angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.EditController", DocumentTypesEditController); -})(); - -angular.module("umbraco") -.controller("Umbraco.Editors.DocumentTypes.MoveController", - function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; + //here we check to see if we've been passed a selected macro and if so we'll set the + //editor to start with parameter editing + if ($scope.model.dialogData && $scope.model.dialogData.macroData) { + $scope.wizardStep = 'paramSelect'; } - - $scope.move = function () { - - $scope.busy = true; - $scope.error = false; - - contentTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "documentTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "documentTypes", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - eventsService.emit('app.refreshEditor'); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } + //get the macro list - pass in a filter if it is only for rte + entityResource.getAll('Macro', $scope.model.dialogData && $scope.model.dialogData.richTextEditor && $scope.model.dialogData.richTextEditor === true ? 'UseInEditor=true' : null).then(function (data) { + if (angular.isArray(data) && data.length == 0) { + $scope.nomacros = true; + } + //if 'allowedMacros' is specified, we need to filter + if (angular.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { + $scope.macros = _.filter(data, function (d) { + return _.contains($scope.model.dialogData.allowedMacros, d.alias); }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); - -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.PropertyController - * @function - * - * @description - * The controller for the content type editor property dialog - */ -(function() { - 'use strict'; - - function PermissionsController($scope, contentTypeResource, iconHelper, contentTypeHelper, localizationService) { - - /* ----------- SCOPE VARIABLES ----------- */ - - var vm = this; - var childNodeSelectorOverlayTitle = ""; - - vm.contentTypes = []; - vm.selectedChildren = []; - - vm.overlayTitle = ""; - - vm.addChild = addChild; - vm.removeChild = removeChild; - - /* ---------- INIT ---------- */ - - init(); - - function init() { - - childNodeSelectorOverlayTitle = localizationService.localize("contentTypeEditor_chooseChildNode"); - - contentTypeResource.getAll().then(function(contentTypes){ - - vm.contentTypes = contentTypes; - - // convert legacy icons - iconHelper.formatContentTypeIcons(vm.contentTypes); - - vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.contentTypes); - - if($scope.model.id === 0) { - contentTypeHelper.insertChildNodePlaceholder(vm.contentTypes, $scope.model.name, $scope.model.icon, $scope.model.id); - } - - }); - - } - - function addChild($event) { - vm.childNodeSelectorOverlay = { - view: "itempicker", - title: childNodeSelectorOverlayTitle, - availableItems: vm.contentTypes, - selectedItems: vm.selectedChildren, - event: $event, - show: true, - submit: function(model) { - vm.selectedChildren.push(model.selectedItem); - $scope.model.allowedContentTypes.push(model.selectedItem.id); - vm.childNodeSelectorOverlay.show = false; - vm.childNodeSelectorOverlay = null; + } else { + $scope.macros = data; + } + //check if there's a pre-selected macro and if it exists + if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroAlias) { + var found = _.find(data, function (item) { + return item.alias === $scope.model.dialogData.macroData.macroAlias; + }); + if (found) { + //select the macro and go to next screen + $scope.model.selectedMacro = found; + editParams(); + return; } - }; - } - - function removeChild(selectedChild, index) { - // remove from vm - vm.selectedChildren.splice(index, 1); - - // remove from content type model - var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); - $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); - } - + } + //we don't have a pre-selected macro so ensure the correct step is set + $scope.wizardStep = 'macroSelect'; + }); } - - angular.module("umbraco").controller("Umbraco.Editors.DocumentType.PermissionsController", PermissionsController); -})(); - -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.TemplatesController - * @function - * - * @description - * The controller for the content type editor templates sub view - */ -(function() { - 'use strict'; - - function TemplatesController($scope, entityResource, contentTypeHelper, $routeParams) { - - /* ----------- SCOPE VARIABLES ----------- */ - - var vm = this; - - vm.availableTemplates = []; - vm.updateTemplatePlaceholder = false; - - - /* ---------- INIT ---------- */ - - init(); - - function init() { - - entityResource.getAll("Template").then(function(templates){ - - vm.availableTemplates = templates; - - // update placeholder template information on new doc types - if (!$routeParams.notemplate && $scope.model.id === 0) { - vm.updateTemplatePlaceholder = true; - vm.availableTemplates = contentTypeHelper.insertTemplatePlaceholder(vm.availableTemplates); - } - - }); - + angular.module('umbraco').controller('Umbraco.Overlays.MacroPickerController', MacroPickerController); + //used for the media picker dialog + angular.module('umbraco').controller('Umbraco.Overlays.MediaPickerController', function ($scope, mediaResource, umbRequestHelper, entityResource, $log, mediaHelper, mediaTypeHelper, eventsService, treeService, $element, $timeout, $cookies, localStorageService, localizationService) { + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectMedia'); + } + var dialogOptions = $scope.model; + $scope.disableFolderSelect = dialogOptions.disableFolderSelect; + $scope.onlyImages = dialogOptions.onlyImages; + $scope.showDetails = dialogOptions.showDetails; + $scope.multiPicker = dialogOptions.multiPicker && dialogOptions.multiPicker !== '0' ? true : false; + $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; + $scope.cropSize = dialogOptions.cropSize; + $scope.lastOpenedNode = localStorageService.get('umbLastOpenedMediaNodeId'); + $scope.lockedFolder = true; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; + var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); + if ($scope.onlyImages) { + $scope.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); + } else { + // Use whitelist of allowed file types if provided + if (allowedUploadFiles !== '') { + $scope.acceptedFileTypes = allowedUploadFiles; + } else { + // If no whitelist, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + $scope.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); + } } - - } - - angular.module("umbraco").controller("Umbraco.Editors.DocumentType.TemplatesController", TemplatesController); -})(); - -/** - * @ngdoc controller - * @name Umbraco.Editors.Media.CreateController - * @function - * - * @description - * The controller for the media creation dialog - */ -function mediaCreateController($scope, $routeParams, mediaTypeResource, iconHelper) { - - mediaTypeResource.getAllowedTypes($scope.currentNode.id).then(function(data) { - $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); - }); - -} - -angular.module('umbraco').controller("Umbraco.Editors.Media.CreateController", mediaCreateController); -/** - * @ngdoc controller - * @name Umbraco.Editors.ContentDeleteController - * @function - * - * @description - * The controller for deleting content - */ -function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, dialogService, notificationsService) { - - $scope.performDelete = function() { - - // stop from firing again on double-click - if ($scope.busy) { return false; } - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - $scope.busy = true; - - mediaResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - treeService.removeNode($scope.currentNode); - - if (rootNode) { - //ensure the recycle bin has child nodes now - var recycleBin = treeService.getDescendantNode(rootNode, -21); - if (recycleBin) { - recycleBin.hasChildren = true; - } - } - - //if the current edited item is the same one as we're deleting, we need to navigate elsewhere - if (editorState.current && editorState.current.id == $scope.currentNode.id) { - - //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent - var location = "/media"; - if ($scope.currentNode.parentId.toString() !== "-1") - location = "/media/media/edit/" + $scope.currentNode.parentId; - - $location.path(location); - } - - navigationService.hideMenu(); - - }, function (err) { - - $scope.currentNode.loading = false; - $scope.busy = false; - - //check if response is ysod - if (err.status && err.status >= 500) { - dialogService.ysodDialog(err); + $scope.maxFileSize = umbracoSettings.maxFileSize + 'KB'; + $scope.model.selectedImages = []; + $scope.acceptedMediatypes = []; + mediaTypeHelper.getAllowedImagetypes($scope.startNodeId).then(function (types) { + $scope.acceptedMediatypes = types; + }); + $scope.searchOptions = { + pageNumber: 1, + pageSize: 100, + totalItems: 0, + totalPages: 0, + filter: '' + }; + //preload selected item + $scope.target = undefined; + if (dialogOptions.currentTarget) { + $scope.target = dialogOptions.currentTarget; + } + function onInit() { + if ($scope.startNodeId !== -1) { + entityResource.getById($scope.startNodeId, 'media').then(function (ent) { + $scope.startNodeId = ent.id; + run(); + }); + } else { + run(); } - - if (err.data && angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); + } + function run() { + //default root item + if (!$scope.target) { + if ($scope.lastOpenedNode && $scope.lastOpenedNode !== -1) { + entityResource.getById($scope.lastOpenedNode, 'media').then(ensureWithinStartNode, gotoStartNode); + } else { + gotoStartNode(); } + } else { + //if a target is specified, go look it up - generally this target will just contain ids not the actual full + //media object so we need to look it up + var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; + var altText = $scope.target.altText; + mediaResource.getById(id).then(function (node) { + $scope.target = node; + if (ensureWithinStartNode(node)) { + selectImage(node); + $scope.target.url = mediaHelper.resolveFile(node); + $scope.target.altText = altText; + $scope.openDetailsDialog(); + } + }, gotoStartNode); } - }); - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Media.DeleteController", MediaDeleteController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.Media.EditController - * @function - * - * @description - * The controller for the media editor - */ -function mediaEditController($scope, $routeParams, appState, mediaResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, treeService, formHelper, umbModelMapper, editorState, umbRequestHelper, $http) { - - //setup scope vars - $scope.currentSection = appState.getSectionState("currentSection"); - $scope.currentNode = null; //the editors affiliated node - - $scope.page = {}; - $scope.page.loading = false; - $scope.page.menu = {}; - $scope.page.menu.currentSection = appState.getSectionState("currentSection"); - $scope.page.menu.currentNode = null; //the editors affiliated node - $scope.page.listViewPath = null; - $scope.page.saveButtonState = "init"; - - /** Syncs the content item to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(content, path, initialLoad) { - - if (!$scope.content.isChildOfListView) { - navigationService.syncTree({ tree: "media", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; - }); - } - else if (initialLoad === true) { - - //it's a child item, just sync the ui node to the parent - navigationService.syncTree({ tree: "media", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node - // from the server so that we can load in the actions menu. - umbRequestHelper.resourcePromise( - $http.get(content.treeNodeUrl), - 'Failed to retrieve data for child node ' + content.id).then(function (node) { - $scope.page.menu.currentNode = node; - }); - } - } - - if ($routeParams.create) { - - $scope.page.loading = true; - - mediaResource.getScaffold($routeParams.id, $routeParams.doctype) - .then(function (data) { - $scope.content = data; - - editorState.set($scope.content); - - $scope.page.loading = false; - - }); - } - else { - - $scope.page.loading = true; - - mediaResource.getById($routeParams.id) - .then(function (data) { - - $scope.content = data; - - if (data.isChildOfListView && data.trashed === false) { - $scope.page.listViewPath = ($routeParams.page) - ? "/media/media/edit/" + data.parentId + "?page=" + $routeParams.page - : "/media/media/edit/" + data.parentId; - } - - editorState.set($scope.content); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - syncTreeNode($scope.content, data.path, true); - - if ($scope.content.parentId && $scope.content.parentId != -1) { - //We fetch all ancestors of the node to generate the footer breadcrump navigation - entityResource.getAncestors($routeParams.id, "media") - .then(function (anc) { - $scope.ancestors = anc; - }); - } - - $scope.page.loading = false; - - }); - } - - $scope.save = function () { - - if (!$scope.busy && formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { - - $scope.busy = true; - $scope.page.saveButtonState = "busy"; - - mediaResource.save($scope.content, $routeParams.create, fileManager.getFiles()) - .then(function(data) { - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - editorState.set($scope.content); - $scope.busy = false; - - syncTreeNode($scope.content, data.path); - - $scope.page.saveButtonState = "success"; - - }, function(err) { - - contentEditingHelper.handleSaveError({ - err: err, - redirectOnFailure: true, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) - }); - - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - - editorState.set($scope.content); - $scope.busy = false; - $scope.page.saveButtonState = "error"; - - }); - }else{ - $scope.busy = false; - } - - }; -} - -angular.module("umbraco") - .controller("Umbraco.Editors.Media.EditController", mediaEditController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.Media.EmptyRecycleBinController - * @function - * - * @description - * The controller for deleting media - */ -function MediaEmptyRecycleBinController($scope, mediaResource, treeService, navigationService, notificationsService, $route) { - - $scope.busy = false; - - $scope.performDelete = function() { - - //(used in the UI) - $scope.busy = true; - $scope.currentNode.loading = true; - - mediaResource.emptyRecycleBin($scope.currentNode.id).then(function (result) { - - $scope.busy = false; - $scope.currentNode.loading = false; - - //show any notifications - if (angular.isArray(result.notifications)) { - for (var i = 0; i < result.notifications.length; i++) { - notificationsService.showNotification(result.notifications[i]); - } - } - - treeService.removeChildNodes($scope.currentNode); - navigationService.hideMenu(); - - //reload the current view - $route.reload(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Media.EmptyRecycleBinController", MediaEmptyRecycleBinController); - -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", - function ($scope, eventsService, mediaResource, appState, treeService, navigationService) { - var dialogOptions = $scope.dialogOptions; - - $scope.dialogTreeEventHandler = $({}); - var node = dialogOptions.currentNode; - - function nodeSelectHandler(ev, args) { - - if(args && args.event) { - args.event.preventDefault(); - args.event.stopPropagation(); - } - - eventsService.emit("editors.media.moveController.select", args); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - } - - function nodeExpandedHandler(ev, args) { - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - } - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.move = function () { - mediaResource.move({ parentId: $scope.target.id, id: node.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "media", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "media", path: activeNodePath, forceReload: false, activate: true }); - } + } + $scope.upload = function (v) { + angular.element('.umb-file-dropzone-directive .file-select').click(); + }; + $scope.dragLeave = function (el, event) { + $scope.activeDrag = false; + }; + $scope.dragEnter = function (el, event) { + $scope.activeDrag = true; + }; + $scope.submitFolder = function () { + if ($scope.newFolderName) { + $scope.creatingFolder = true; + mediaResource.addFolder($scope.newFolderName, $scope.currentFolder.id).then(function (data) { + //we've added a new folder so lets clear the tree cache for that specific item + treeService.clearCache({ + cacheKey: '__media', + //this is the main media tree cache key + childrenOf: data.parentId }); - - }, function (err) { - $scope.success = false; - $scope.error = err; + $scope.creatingFolder = false; + $scope.gotoFolder(data); + $scope.showFolderInput = false; + $scope.newFolderName = ''; }); - }; - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - - // Mini list view - $scope.selectListViewNode = function (node) { - node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - }); -/** - * @ngdoc controller - * @name Umbraco.Editors.Content.MediaRecycleBinController - * @function - * - * @description - * Controls the recycle bin for media - * - */ - -function MediaRecycleBinController($scope, $routeParams, mediaResource, navigationService, localizationService) { - - //ensures the list view doesn't actually load until we query for the list view config - // for the section - $scope.page = {}; - $scope.page.name = "Recycle Bin"; - $scope.page.nameLocked = true; - - //ensures the list view doesn't actually load until we query for the list view config - // for the section - $scope.listViewPath = null; - - $routeParams.id = "-21"; - mediaResource.getRecycleBin().then(function (result) { - //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a - // single property, so we'll extract that property (list view) and use it's data. - var listproperty = result.tabs[0].properties[0]; - - _.each(listproperty.config, function (val, key) { - $scope.model.config[key] = val; - }); - $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; - }); - - $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; - - // sync tree node - navigationService.syncTree({ tree: "media", path: ["-1", $routeParams.id], forceReload: false }); - - localizePageName(); - - function localizePageName() { - - var pageName = "general_recycleBin"; - - localizationService.localize(pageName).then(function (value) { - $scope.page.name = value; - }); - - } -} - -angular.module('umbraco').controller("Umbraco.Editors.Media.RecycleBinController", MediaRecycleBinController); - -angular.module("umbraco") -.controller("Umbraco.Editors.MediaTypes.CopyController", - function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; + } else { + $scope.showFolderInput = false; } - - $scope.target = args.node; - $scope.target.selected = true; - } - - $scope.copy = function () { - - $scope.busy = true; - $scope.error = false; - - mediaTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the copied content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was copied!!) - - navigationService.syncTree({ tree: "mediaTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "mediaTypes", path: activeNodePath, forceReload: false, activate: true }); - } + }; + $scope.enterSubmitFolder = function (event) { + if (event.keyCode === 13) { + $scope.submitFolder(); + event.stopPropagation(); + } + }; + $scope.gotoFolder = function (folder) { + if (!$scope.multiPicker) { + deselectAllImages($scope.model.selectedImages); + } + if (!folder) { + folder = { + id: -1, + name: 'Media', + icon: 'icon-folder' + }; + } + if (folder.id > 0) { + entityResource.getAncestors(folder.id, 'media').then(function (anc) { + $scope.path = _.filter(anc, function (f) { + return f.path.indexOf($scope.startNodeId) !== -1; }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } }); + mediaTypeHelper.getAllowedImagetypes(folder.id).then(function (types) { + $scope.acceptedMediatypes = types; + }); + } else { + $scope.path = []; + } + $scope.lockedFolder = folder.id === -1 && $scope.model.startNodeIsVirtual; + $scope.currentFolder = folder; + localStorageService.set('umbLastOpenedMediaNodeId', folder.id); + return getChildren(folder.id); }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); - -/** - * @ngdoc controller - * @name Umbraco.Editors.MediaType.CreateController - * @function - * - * @description - * The controller for the media type creation dialog - */ -function MediaTypesCreateController($scope, $location, navigationService, mediaTypeResource, formHelper, appState, localizationService) { - - $scope.model = { - folderName: "", - creatingFolder: false - }; - - var node = $scope.dialogOptions.currentNode, - localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); - - $scope.showCreateFolder = function() { - $scope.model.creatingFolder = true; - } - - $scope.createContainer = function () { - if (formHelper.submitForm({ - scope: $scope, - formCtrl: this.createFolderForm, - statusMessage: localizeCreateFolder - })) { - mediaTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { - - navigationService.hideMenu(); - var currPath = node.path ? node.path : "-1"; - navigationService.syncTree({ tree: "mediatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - - formHelper.resetForm({ scope: $scope }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - //TODO: Handle errors - }); + $scope.clickHandler = function (image, event, index) { + if (image.isFolder) { + if ($scope.disableFolderSelect) { + $scope.gotoFolder(image); + } else { + eventsService.emit('dialogs.mediaPicker.select', image); + selectImage(image); + } + } else { + eventsService.emit('dialogs.mediaPicker.select', image); + if ($scope.showDetails) { + $scope.target = image; + // handle both entity and full media object + if (image.image) { + $scope.target.url = image.image; + } else { + $scope.target.url = mediaHelper.resolveFile(image); + } + $scope.openDetailsDialog(); + } else { + selectImage(image); + } + } }; - } - - $scope.createMediaType = function() { - $location.search('create', null); - $location.path("/settings/mediatypes/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - } -} - -angular.module('umbraco').controller("Umbraco.Editors.MediaTypes.CreateController", MediaTypesCreateController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.MediaType.DeleteController - * @function - * - * @description - * The controller for the media type delete dialog - */ -function MediaTypesDeleteController($scope, dataTypeResource, mediaTypeResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - mediaTypeResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.performContainerDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - mediaTypeResource.deleteContainerById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.DeleteController", MediaTypesDeleteController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.MediaType.EditController - * @function - * - * @description - * The controller for the media type editor - */ -(function () { - "use strict"; - - function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { - - var vm = this; - var localizeSaving = localizationService.localize("general_saving"); - var evts = []; - - vm.save = save; - - vm.currentNode = null; - vm.contentType = {}; - vm.page = {}; - vm.page.loading = false; - vm.page.saveButtonState = "init"; - vm.page.navigation = [ - { - "name": localizationService.localize("general_design"), - "icon": "icon-document-dashed-line", - "view": "views/mediatypes/views/design/design.html", - "active": true - }, - { - "name": localizationService.localize("general_listView"), - "icon": "icon-list", - "view": "views/mediatypes/views/listview/listview.html" - }, - { - "name": localizationService.localize("general_rights"), - "icon": "icon-keychain", - "view": "views/mediatypes/views/permissions/permissions.html" - } - ]; - - vm.page.keyboardShortcutsOverview = [ - { - "name": localizationService.localize("main_sections"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_navigateSections"), - "keys": [{ "key": "1" }, { "key": "3" }], - "keyRange": true - } - ] - }, - { - "name": localizationService.localize("general_design"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_addTab"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] - }, - { - "description": localizationService.localize("shortcuts_addProperty"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] - }, - { - "description": localizationService.localize("shortcuts_addEditor"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] - }, - { - "description": localizationService.localize("shortcuts_editDataType"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] - } - ] - }, - { - "name": localizationService.localize("general_listView"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_toggleListView"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] - } - ] - }, - { - "name": localizationService.localize("general_rights"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_toggleAllowAsRoot"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] - }, - { - "description": localizationService.localize("shortcuts_addChildNode"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] - } - ] - } - ]; - - contentTypeHelper.checkModelsBuilderStatus().then(function (result) { - vm.page.modelsBuilder = result; - if (result) { - //Models builder mode: - vm.page.defaultButton = { - hotKey: "ctrl+s", - hotKeyWhenHidden: true, - labelKey: "buttons_save", - letter: "S", - type: "submit", - handler: function () { vm.save(); } - }; - vm.page.subButtons = [{ - hotKey: "ctrl+g", - hotKeyWhenHidden: true, - labelKey: "buttons_saveAndGenerateModels", - letter: "G", - handler: function () { - - vm.page.saveButtonState = "busy"; - - vm.save().then(function (result) { - - vm.page.saveButtonState = "busy"; - - localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { - localizationService.localize("modelsBuilder_waitingMessage").then(function(msgValue) { - notificationsService.info(headerValue, msgValue); - }); - }); - - contentTypeHelper.generateModels().then(function (result) { - - if (result.success) { - - //re-check model status - contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { - vm.page.modelsBuilder = statusResult; - }); - - //clear and add success - vm.page.saveButtonState = "init"; - localizationService.localize("modelsBuilder_modelsGenerated").then(function(value) { - notificationsService.success(value); - }); - - } else { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function(value) { - notificationsService.error(value); - }); - } - - }, function () { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsGeneratedError").then(function(value) { - notificationsService.error(value); - }); - }); - - }); - - } - }]; - } - }); - - if ($routeParams.create) { - vm.page.loading = true; - - //we are creating so get an empty data type item - mediaTypeResource.getScaffold($routeParams.id) - .then(function(dt) { - init(dt); - - vm.page.loading = false; - }); - } - else { - loadMediaType(); - } - - function loadMediaType() { - vm.page.loading = true; - - mediaTypeResource.getById($routeParams.id).then(function(dt) { - init(dt); - - syncTreeNode(vm.contentType, dt.path, true); - - vm.page.loading = false; - }); - } - - /* ---------- SAVE ---------- */ - - function save() { - - // only save if there is no overlays open - if(overlayHelper.getNumberOfOverlays() === 0) { - - var deferred = $q.defer(); - - vm.page.saveButtonState = "busy"; - - // reformat allowed content types to array if id's - vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: localizeSaving, - saveMethod: mediaTypeResource.save, - scope: $scope, - content: vm.contentType, - //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - // we need to rebind... the IDs that have been created! - rebindCallback: function (origContentType, savedContentType) { - vm.contentType.id = savedContentType.id; - vm.contentType.groups.forEach(function (group) { - if (!group.name) return; - - var k = 0; - while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) - k++; - if (k == savedContentType.groups.length) { - group.id = 0; - return; - } - - var savedGroup = savedContentType.groups[k]; - if (!group.id) group.id = savedGroup.id; - - group.properties.forEach(function (property) { - if (property.id || !property.alias) return; - - k = 0; - while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) - k++; - if (k == savedGroup.properties.length) { - property.id = 0; - return; - } - - var savedProperty = savedGroup.properties[k]; - property.id = savedProperty.id; - }); - }); - } - }).then(function (data) { - //success - syncTreeNode(vm.contentType, data.path); - - vm.page.saveButtonState = "success"; - - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } - else { - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); - } - - vm.page.saveButtonState = "error"; - - deferred.reject(err); - }); - - return deferred.promise; - } - } - - function init(contentType) { - - // set all tab to inactive - if (contentType.groups.length !== 0) { - angular.forEach(contentType.groups, function (group) { - - angular.forEach(group.properties, function (property) { - // get data type details for each property - getDataTypeDetails(property); - }); - - }); - } - - // convert icons for content type - convertLegacyIcons(contentType); - - //set a shared state - editorState.set(contentType); - - vm.contentType = contentType; - } - - function convertLegacyIcons(contentType) { - // make array to store contentType icon - var contentTypeArray = []; - - // push icon to array - contentTypeArray.push({ "icon": contentType.icon }); - - // run through icon method - iconHelper.formatContentTypeIcons(contentTypeArray); - - // set icon back on contentType - contentType.icon = contentTypeArray[0].icon; - } - - function getDataTypeDetails(property) { - if (property.propertyState !== "init") { - - dataTypeResource.getById(property.dataTypeId) - .then(function(dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - } - } - - - /** Syncs the content type to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(dt, path, initialLoad) { - navigationService.syncTree({ tree: "mediatypes", path: path.split(","), forceReload: initialLoad !== true }).then(function(syncArgs) { - vm.currentNode = syncArgs.node; - }); - } - - evts.push(eventsService.on("app.refreshEditor", function(name, error) { - loadMediaType(); - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); - } - - angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.EditController", MediaTypesEditController); -})(); - -angular.module("umbraco") -.controller("Umbraco.Editors.MediaTypes.MoveController", - function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; + $scope.clickItemName = function (item) { + if (item.isFolder) { + $scope.gotoFolder(item); } - - $scope.target = args.node; - $scope.target.selected = true; - } - - $scope.move = function () { - - $scope.busy = true; - $scope.error = false; - - mediaTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "mediaTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "mediaTypes", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - eventsService.emit('app.refreshEditor'); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); - -(function() { - 'use strict'; - - function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService) { - - /* ----------- SCOPE VARIABLES ----------- */ - - var vm = this; - var childNodeSelectorOverlayTitle = ""; - - vm.mediaTypes = []; - vm.selectedChildren = []; - - vm.addChild = addChild; - vm.removeChild = removeChild; - - /* ---------- INIT ---------- */ - - init(); - - function init() { - - childNodeSelectorOverlayTitle = localizationService.localize("contentTypeEditor_chooseChildNode"); - - mediaTypeResource.getAll().then(function(mediaTypes){ - - vm.mediaTypes = mediaTypes; - - // convert legacy icons - iconHelper.formatContentTypeIcons(vm.mediaTypes); - - vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.mediaTypes); - - if($scope.model.id === 0) { - contentTypeHelper.insertChildNodePlaceholder(vm.mediaTypes, $scope.model.name, $scope.model.icon, $scope.model.id); - } - - }); - - } - - function addChild($event) { - vm.childNodeSelectorOverlay = { - view: "itempicker", - title: childNodeSelectorOverlayTitle, - availableItems: vm.mediaTypes, - selectedItems: vm.selectedChildren, - event: $event, - show: true, - submit: function(model) { - vm.selectedChildren.push(model.selectedItem); - $scope.model.allowedContentTypes.push(model.selectedItem.id); - vm.childNodeSelectorOverlay.show = false; - vm.childNodeSelectorOverlay = null; + function selectImage(image) { + if (image.selected) { + for (var i = 0; $scope.model.selectedImages.length > i; i++) { + var imageInSelection = $scope.model.selectedImages[i]; + if (image.key === imageInSelection.key) { + image.selected = false; + $scope.model.selectedImages.splice(i, 1); + } } - }; + } else { + if (!$scope.multiPicker) { + deselectAllImages($scope.model.selectedImages); + } + image.selected = true; + $scope.model.selectedImages.push(image); + } } - - function removeChild(selectedChild, index) { - // remove from vm - vm.selectedChildren.splice(index, 1); - - // remove from content type model - var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); - $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); + function deselectAllImages(images) { + for (var i = 0; i < images.length; i++) { + var image = images[i]; + image.selected = false; + } + images.length = 0; } - - } - - angular.module("umbraco").controller("Umbraco.Editors.MediaType.PermissionsController", PermissionsController); -})(); - -/** - * @ngdoc controller - * @name Umbraco.Editors.Member.CreateController - * @function - * - * @description - * The controller for the member creation dialog - */ -function memberCreateController($scope, $routeParams, memberTypeResource, iconHelper) { - - memberTypeResource.getTypes($scope.currentNode.id).then(function (data) { - $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); - }); - -} - -angular.module('umbraco').controller("Umbraco.Editors.Member.CreateController", memberCreateController); -/** - * @ngdoc controller - * @name Umbraco.Editors.Member.DeleteController - * @function - * - * @description - * The controller for deleting content - */ -function MemberDeleteController($scope, memberResource, treeService, navigationService, editorState, $location, $routeParams) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - - memberResource.deleteByKey($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - treeService.removeNode($scope.currentNode); - - //if the current edited item is the same one as we're deleting, we need to navigate elsewhere - if (editorState.current && editorState.current.key == $scope.currentNode.id) { - $location.path("/member/member/list/" + ($routeParams.listName ? $routeParams.listName : 'all-members')); - } - - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Member.DeleteController", MemberDeleteController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.Member.EditController - * @function - * - * @description - * The controller for the member editor - */ -function MemberEditController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper, editorState, umbRequestHelper, $http) { - - //setup scope vars - $scope.page = {}; - $scope.page.loading = true; - $scope.page.menu = {}; - $scope.page.menu.currentSection = appState.getSectionState("currentSection"); - $scope.page.menu.currentNode = null; //the editors affiliated node - $scope.page.nameLocked = false; - $scope.page.listViewPath = null; - $scope.page.saveButtonState = "init"; - $scope.busy = false; - - $scope.page.listViewPath = ($routeParams.page && $routeParams.listName) - ? "/member/member/list/" + $routeParams.listName + "?page=" + $routeParams.page - : null; - - //build a path to sync the tree with - function buildTreePath(data) { - return $routeParams.listName ? "-1," + $routeParams.listName : "-1"; - } - - if ($routeParams.create) { - - //if there is no doc type specified then we are going to assume that - // we are not using the umbraco membership provider - if ($routeParams.doctype) { - - //we are creating so get an empty member item - memberResource.getScaffold($routeParams.doctype) - .then(function(data) { - - $scope.content = data; - - setHeaderNameState($scope.content); - - editorState.set($scope.content); - - $scope.page.loading = false; - - }); - } - else { - - memberResource.getScaffold() - .then(function (data) { - $scope.content = data; - - setHeaderNameState($scope.content); - - editorState.set($scope.content); - - $scope.page.loading = false; - - }); - } - - } - else { - //so, we usually refernce all editors with the Int ID, but with members we have - //a different pattern, adding a route-redirect here to handle this: - //isNumber doesnt work here since its seen as a string - - //TODO: Why is this here - I don't understand why this would ever be an integer? This will not work when we support non-umbraco membership providers. - - if ($routeParams.id && $routeParams.id.length < 9) { - - entityResource.getById($routeParams.id, "Member").then(function(entity) { - $location.path("/member/member/edit/" + entity.key); - }); - } - else { - - //we are editing so get the content item from the server - memberResource.getByKey($routeParams.id) - .then(function(data) { - - $scope.content = data; - - setHeaderNameState($scope.content); - - editorState.set($scope.content); - - var path = buildTreePath(data); - - //sync the tree (only for ui purposes) - navigationService.syncTree({ tree: "member", path: path.split(",") }); - - //it's the initial load of the editor, we need to get the tree node - // from the server so that we can load in the actions menu. - umbRequestHelper.resourcePromise( - $http.get(data.treeNodeUrl), - 'Failed to retrieve data for child node ' + data.key).then(function (node) { - $scope.page.menu.currentNode = node; - }); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - $scope.page.loading = false; - - }); - } - - } - - function setHeaderNameState(content) { - - if(content.membershipScenario === 0) { - $scope.page.nameLocked = true; - } - - } - - $scope.save = function() { - - if (!$scope.busy && formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { - - $scope.busy = true; - $scope.page.saveButtonState = "busy"; - - memberResource.save($scope.content, $routeParams.create, fileManager.getFiles()) - .then(function(data) { - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - //specify a custom id to redirect to since we want to use the GUID - redirectId: data.key, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - editorState.set($scope.content); - $scope.busy = false; - $scope.page.saveButtonState = "success"; - - var path = buildTreePath(data); - - //sync the tree (only for ui purposes) - navigationService.syncTree({ tree: "member", path: path.split(","), forceReload: true }); - - }, function (err) { - - contentEditingHelper.handleSaveError({ - redirectOnFailure: false, - err: err, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) - }); - - editorState.set($scope.content); - $scope.busy = false; - $scope.page.saveButtonState = "error"; - - }); - }else{ - $scope.busy = false; - } - - }; - -} - -angular.module("umbraco").controller("Umbraco.Editors.Member.EditController", MemberEditController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.Member.ListController - * @function - * - * @description - * The controller for the member list view - */ -function MemberListController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper, editorState, localizationService) { - - //setup scope vars - $scope.currentSection = appState.getSectionState("currentSection"); - $scope.currentNode = null; //the editors affiliated node - - $scope.page = {}; - $scope.page.lockedName = true; - $scope.page.loading = true; - - //we are editing so get the content item from the server - memberResource.getListNode($routeParams.id) - .then(function (data) { - - $scope.content = data; - - //translate "All Members" - if ($scope.content != null && $scope.content.name != null && $scope.content.name.replace(" ", "").toLowerCase() == "allmembers") { - localizationService.localize("member_allMembers").then(function (value) { - $scope.content.name = value; + $scope.onUploadComplete = function (files) { + $scope.gotoFolder($scope.currentFolder).then(function () { + if (files.length === 1 && $scope.model.selectedImages.length === 0) { + selectImage($scope.images[$scope.images.length - 1]); + } + }); + }; + $scope.onFilesQueue = function () { + $scope.activeDrag = false; + }; + function ensureWithinStartNode(node) { + // make sure that last opened node is on the same path as start node + var nodePath = node.path.split(','); + if (nodePath.indexOf($scope.startNodeId.toString()) !== -1) { + $scope.gotoFolder({ + id: $scope.lastOpenedNode, + name: 'Media', + icon: 'icon-folder' + }); + return true; + } else { + $scope.gotoFolder({ + id: $scope.startNodeId, + name: 'Media', + icon: 'icon-folder' }); + return false; } - - editorState.set($scope.content); - - navigationService.syncTree({ tree: "member", path: data.path.split(",") }).then(function (syncArgs) { - $scope.currentNode = syncArgs.node; + } + function gotoStartNode(err) { + $scope.gotoFolder({ + id: $scope.startNodeId, + name: 'Media', + icon: 'icon-folder' }); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - $scope.page.loading = false; - - }); -} - -angular.module("umbraco").controller("Umbraco.Editors.Member.ListController", MemberListController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.MemberType.CreateController - * @function - * - * @description - * The controller for the member type creation dialog - */ -function MemberTypesCreateController($scope, $location, navigationService, memberTypeResource, formHelper, appState, localizationService) { - - $scope.model = { - folderName: "", - creatingFolder: false - }; - - var node = $scope.dialogOptions.currentNode, - localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); - - - $scope.showCreateFolder = function() { - $scope.model.creatingFolder = true; - } - - $scope.createContainer = function () { - if (formHelper.submitForm({ - scope: $scope, - formCtrl: this.createFolderForm, - statusMessage: localizeCreateFolder - })) { - memberTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { - - navigationService.hideMenu(); - var currPath = node.path ? node.path : "-1"; - navigationService.syncTree({ tree: "membertypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - - formHelper.resetForm({ scope: $scope }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - //TODO: Handle errors + } + $scope.openDetailsDialog = function () { + $scope.mediaPickerDetailsOverlay = {}; + $scope.mediaPickerDetailsOverlay.show = true; + $scope.mediaPickerDetailsOverlay.submit = function (model) { + $scope.model.selectedImages.push($scope.target); + $scope.model.submit($scope.model); + $scope.mediaPickerDetailsOverlay.show = false; + $scope.mediaPickerDetailsOverlay = null; + }; + $scope.mediaPickerDetailsOverlay.close = function (oldModel) { + $scope.mediaPickerDetailsOverlay.show = false; + $scope.mediaPickerDetailsOverlay = null; + }; + }; + var debounceSearchMedia = _.debounce(function () { + $scope.$apply(function () { + if ($scope.searchOptions.filter) { + searchMedia(); + } else { + // reset pagination + $scope.searchOptions = { + pageNumber: 1, + pageSize: 100, + totalItems: 0, + totalPages: 0, + filter: '' + }; + getChildren($scope.currentFolder.id); + } }); + }, 500); + $scope.changeSearch = function () { + $scope.loading = true; + debounceSearchMedia(); }; - } - - $scope.createMemberType = function() { - $location.search('create', null); - $location.path("/settings/membertypes/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - } -} - -angular.module('umbraco').controller("Umbraco.Editors.MemberTypes.CreateController", MemberTypesCreateController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.MemberTypes.DeleteController - * @function - * - * @description - * The controller for deleting member types - */ -function MemberTypesDeleteController($scope, memberTypeResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - memberTypeResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.MemberTypes.DeleteController", MemberTypesDeleteController); - -/** - * @ngdoc controller - * @name Umbraco.Editors.MemberType.EditController - * @function - * - * @description - * The controller for the member type editor - */ -(function () { - "use strict"; - - function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q, localizationService, overlayHelper, contentTypeHelper) { - - var vm = this; - var localizeSaving = localizationService.localize("general_saving"); - - vm.save = save; - - vm.currentNode = null; - vm.contentType = {}; - vm.page = {}; - vm.page.loading = false; - vm.page.saveButtonState = "init"; - vm.page.navigation = [ - { - "name": localizationService.localize("general_design"), - "icon": "icon-document-dashed-line", - "view": "views/membertypes/views/design/design.html", - "active": true - } - ]; - - vm.page.keyboardShortcutsOverview = [ - { - "name": localizationService.localize("shortcuts_shortcut"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_addTab"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] - }, - { - "description": localizationService.localize("shortcuts_addProperty"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] - }, - { - "description": localizationService.localize("shortcuts_addEditor"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] - }, - { - "description": localizationService.localize("shortcuts_editDataType"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] - } - ] - } - ]; - - contentTypeHelper.checkModelsBuilderStatus().then(function (result) { - vm.page.modelsBuilder = result; - if (result) { - //Models builder mode: - vm.page.defaultButton = { - hotKey: "ctrl+s", - hotKeyWhenHidden: true, - labelKey: "buttons_save", - letter: "S", - type: "submit", - handler: function () { vm.save(); } - }; - vm.page.subButtons = [{ - hotKey: "ctrl+g", - hotKeyWhenHidden: true, - labelKey: "buttons_saveAndGenerateModels", - letter: "G", - handler: function () { - - vm.page.saveButtonState = "busy"; - - vm.save().then(function (result) { - - vm.page.saveButtonState = "busy"; - - localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { - localizationService.localize("modelsBuilder_waitingMessage").then(function(msgValue) { - notificationsService.info(headerValue, msgValue); - }); - }); - - contentTypeHelper.generateModels().then(function (result) { - - if (result.success) { - - //re-check model status - contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { - vm.page.modelsBuilder = statusResult; - }); - - //clear and add success - vm.page.saveButtonState = "init"; - localizationService.localize("modelsBuilder_modelsGenerated").then(function(value) { - notificationsService.success(value); - }); - - } else { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function(value) { - notificationsService.error(value); - }); - } - - }, function () { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsGeneratedError").then(function(value) { - notificationsService.error(value); - }); - }); - - - }); - + $scope.changePagination = function (pageNumber) { + $scope.loading = true; + $scope.searchOptions.pageNumber = pageNumber; + searchMedia(); + }; + function searchMedia() { + $scope.loading = true; + entityResource.getPagedDescendants($scope.startNodeId, 'Media', $scope.searchOptions).then(function (data) { + // update image data to work with image grid + angular.forEach(data.items, function (mediaItem) { + // set thumbnail and src + mediaItem.thumbnail = mediaHelper.resolveFileFromEntity(mediaItem, true); + mediaItem.image = mediaHelper.resolveFileFromEntity(mediaItem, false); + // set properties to match a media object + if (mediaItem.metaData && mediaItem.metaData.umbracoWidth && mediaItem.metaData.umbracoHeight) { + mediaItem.properties = [ + { + alias: 'umbracoWidth', + value: mediaItem.metaData.umbracoWidth.Value + }, + { + alias: 'umbracoHeight', + value: mediaItem.metaData.umbracoHeight.Value + } + ]; } - }]; - } - }); - - if ($routeParams.create) { - - vm.page.loading = true; - - //we are creating so get an empty data type item - memberTypeResource.getScaffold($routeParams.id) - .then(function (dt) { - init(dt); - - vm.page.loading = false; - }); - } - else { - - vm.page.loading = true; - - memberTypeResource.getById($routeParams.id).then(function (dt) { - init(dt); - - syncTreeNode(vm.contentType, dt.path, true); - - vm.page.loading = false; + }); + // update images + $scope.images = data.items ? data.items : []; + // update pagination + if (data.pageNumber > 0) + $scope.searchOptions.pageNumber = data.pageNumber; + if (data.pageSize > 0) + $scope.searchOptions.pageSize = data.pageSize; + $scope.searchOptions.totalItems = data.totalItems; + $scope.searchOptions.totalPages = data.totalPages; + // set already selected images to selected + preSelectImages(); + $scope.loading = false; }); } - - function save() { - // only save if there is no overlays open - if(overlayHelper.getNumberOfOverlays() === 0) { - - var deferred = $q.defer(); - - vm.page.saveButtonState = "busy"; - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: localizeSaving, - saveMethod: memberTypeResource.save, - scope: $scope, - content: vm.contentType, - //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - // we need to rebind... the IDs that have been created! - rebindCallback: function (origContentType, savedContentType) { - vm.contentType.id = savedContentType.id; - vm.contentType.groups.forEach(function (group) { - if (!group.name) return; - - var k = 0; - while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) - k++; - if (k == savedContentType.groups.length) { - group.id = 0; - return; - } - - var savedGroup = savedContentType.groups[k]; - if (!group.id) group.id = savedGroup.id; - - group.properties.forEach(function (property) { - if (property.id || !property.alias) return; - - k = 0; - while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) - k++; - if (k == savedGroup.properties.length) { - property.id = 0; - return; - } - - var savedProperty = savedGroup.properties[k]; - property.id = savedProperty.id; - }); - }); - } - }).then(function (data) { - //success - syncTreeNode(vm.contentType, data.path); - - vm.page.saveButtonState = "success"; - - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } - else { - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); - } - - vm.page.saveButtonState = "error"; - - deferred.reject(err); - }); - - return deferred.promise; - } - - } - - function init(contentType) { - - // set all tab to inactive - if (contentType.groups.length !== 0) { - angular.forEach(contentType.groups, function (group) { - - angular.forEach(group.properties, function (property) { - // get data type details for each property - getDataTypeDetails(property); - }); - - }); - } - - // convert legacy icons - convertLegacyIcons(contentType); - - //set a shared state - editorState.set(contentType); - - vm.contentType = contentType; - - } - - function convertLegacyIcons(contentType) { - - // make array to store contentType icon - var contentTypeArray = []; - - // push icon to array - contentTypeArray.push({ "icon": contentType.icon }); - - // run through icon method - iconHelper.formatContentTypeIcons(contentTypeArray); - - // set icon back on contentType - contentType.icon = contentTypeArray[0].icon; - - } - - function getDataTypeDetails(property) { - - if (property.propertyState !== "init") { - - dataTypeResource.getById(property.dataTypeId) - .then(function (dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - } - } - - /** Syncs the content type to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(dt, path, initialLoad) { - - navigationService.syncTree({ tree: "membertypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - vm.currentNode = syncArgs.node; + function getChildren(id) { + $scope.loading = true; + return mediaResource.getChildren(id).then(function (data) { + $scope.searchOptions.filter = ''; + $scope.images = data.items ? data.items : []; + // set already selected images to selected + preSelectImages(); + $scope.loading = false; }); - } - - - } - - angular.module("umbraco").controller("Umbraco.Editors.MemberTypes.EditController", MemberTypesEditController); - -})(); - -angular.module("umbraco") -.controller("Umbraco.Editors.MemberTypes.MoveController", - function($scope){ - + function preSelectImages() { + for (var folderImageIndex = 0; folderImageIndex < $scope.images.length; folderImageIndex++) { + var folderImage = $scope.images[folderImageIndex]; + var imageIsSelected = false; + if ($scope.model && angular.isArray($scope.model.selectedImages)) { + for (var selectedImageIndex = 0; selectedImageIndex < $scope.model.selectedImages.length; selectedImageIndex++) { + var selectedImage = $scope.model.selectedImages[selectedImageIndex]; + if (folderImage.key === selectedImage.key) { + imageIsSelected = true; + } + } + } + if (imageIsSelected) { + folderImage.selected = true; + } + } + } + onInit(); }); - -/** - * @ngdoc controller - * @name Umbraco.Editors.Packages.DeleteController - * @function - * - * @description - * The controller for deleting content - */ -function PackageDeleteController($scope, packageResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - packageResource.deleteCreatedPackage($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); + angular.module('umbraco').controller('Umbraco.Overlays.MediaTypePickerController', function ($scope) { + $scope.select = function (mediatype) { + $scope.model.selectedType = mediatype; + $scope.model.submit($scope.model); + $scope.model.show = false; + }; + }); + //used for the member picker dialog + angular.module('umbraco').controller('Umbraco.Overlays.MemberGroupPickerController', function ($scope, eventsService, entityResource, searchService, $log, localizationService) { + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectMemberGroup'); + } + $scope.dialogTreeEventHandler = $({}); + $scope.multiPicker = $scope.model.multiPicker; + function activate() { + if ($scope.multiPicker) { + $scope.model.selectedMemberGroups = []; + } else { + $scope.model.selectedMemberGroup = ''; + } + } + function selectMemberGroup(id) { + $scope.model.selectedMemberGroup = id; + } + function selectMemberGroups(id) { + $scope.model.selectedMemberGroups.push(id); + } + /** Method used for selecting a node */ + function select(text, id) { + if ($scope.model.multiPicker) { + selectMemberGroups(id); + } else { + selectMemberGroup(id); + $scope.model.submit($scope.model); + } + } + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + eventsService.emit('dialogs.memberGroupPicker.select', args); + //This is a tree node, so we don't have an entity to pass in, it will need to be looked up + //from the server in this method. + select(args.node.name, args.node.id); + //toggle checked state + args.node.selected = args.node.selected === true ? false : true; + } + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Packages.DeleteController", PackageDeleteController); - -(function () { - "use strict"; - - function PackagesOverviewController($scope, $route, $location, navigationService, $timeout, localStorageService) { - - //Hack! - // if there is a cookie value for packageInstallUri then we need to redirect there, - // the issue is that we still have webforms and we cannot go to a hash location and then window.reload - // because it will double load it. - // we will refresh and then navigate there. - - var installPackageUri = localStorageService.get("packageInstallUri"); - if (installPackageUri) { - localStorageService.remove("packageInstallUri"); - } - if (installPackageUri && installPackageUri !== "installed") { - //navigate to the custom installer screen, if it is just "installed", then we'll - //show the installed view - $location.path(installPackageUri).search(""); - } - else { + activate(); + }); + (function () { + 'use strict'; + function MoveOverlay($scope, localizationService, eventsService, entityHelper) { var vm = this; - - vm.page = {}; - vm.page.name = "Packages"; - vm.page.navigation = [ - { - "name": "Packages", - "icon": "icon-cloud", - "view": "views/packager/views/repo.html", - "active": !installPackageUri || installPackageUri === "navigation" - }, - { - "name": "Installed", - "icon": "icon-box", - "view": "views/packager/views/installed.html", - "active": installPackageUri === "installed" - }, - { - "name": "Install local", - "icon": "icon-add", - "view": "views/packager/views/install-local.html", - "active": installPackageUri === "local" - } - ]; - - $timeout(function () { - navigationService.syncTree({ tree: "packager", path: "-1" }); + vm.hideSearch = hideSearch; + vm.selectResult = selectResult; + vm.onSearchResults = onSearchResults; + var dialogOptions = $scope.model; + var searchText = 'Search...'; + var node = dialogOptions.currentNode; + localizationService.localize('general_search').then(function (value) { + searchText = value + '...'; }); - } - - } - - angular.module("umbraco").controller("Umbraco.Editors.Packages.OverviewController", PackagesOverviewController); - -})(); - -(function () { - "use strict"; - - function PackagesInstallLocalController($scope, $route, $location, Upload, umbRequestHelper, packageResource, localStorageService, $timeout, $window, localizationService) { - - var vm = this; - vm.state = "upload"; - - vm.localPackage = {}; - vm.installPackage = installPackage; - vm.installState = { - status: "", - progress:0 - }; - vm.installCompleted = false; - vm.zipFile = { - uploadStatus: "idle", - uploadProgress: 0, - serverErrorMessage: null - }; - - $scope.handleFiles = function (files, event) { - if (files) { - for (var i = 0; i < files.length; i++) { - upload(files[i]); - } + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('actions_move'); } - }; - - function upload(file) { - - Upload.upload({ - url: umbRequestHelper.getApiUrl("packageInstallApiBaseUrl", "UploadLocalPackage"), - fields: {}, - file: file - }).progress(function (evt) { - - // hack: in some browsers the progress event is called after success - // this prevents the UI from going back to a uploading state - if(vm.zipFile.uploadStatus !== "done" && vm.zipFile.uploadStatus !== "error") { - - // set view state to uploading - vm.state = 'uploading'; - - // calculate progress in percentage - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); - - // set percentage property on file - vm.zipFile.uploadProgress = progressPercentage; - - // set uploading status on file - vm.zipFile.uploadStatus = "uploading"; - + $scope.model.relateToOriginal = true; + $scope.dialogTreeEventHandler = $({}); + vm.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + // get entity type based on the section + $scope.entityType = entityHelper.getEntityTypeFromSection(dialogOptions.section); + function nodeSelectHandler(ev, args) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); } - - }).success(function (data, status, headers, config) { - - if (data.notifications && data.notifications.length > 0) { - - // set error status on file - vm.zipFile.uploadStatus = "error"; - - // Throw message back to user with the cause of the error - vm.zipFile.serverErrorMessage = data.notifications[0].message; - - } else { - - // set done status on file - vm.zipFile.uploadStatus = "done"; - loadPackage(); - vm.localPackage = data; + //eventsService.emit("editors.content.copyController.select", args); + if ($scope.model.target) { + //un-select if there's a current one selected + $scope.model.target.selected = false; } - - }).error(function (evt, status, headers, config) { - - // set status done - vm.zipFile.uploadStatus = "error"; - - // If file not found, server will return a 404 and display this message - if (status === 404) { - vm.zipFile.serverErrorMessage = "File not found"; + $scope.model.target = args.node; + $scope.model.target.selected = true; + } + function nodeExpandedHandler(ev, args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); } - else if (status == 400) { - //it's a validation error - vm.zipFile.serverErrorMessage = evt.message; + } + function hideSearch() { + vm.searchInfo.showSearch = false; + vm.searchInfo.searchFromId = null; + vm.searchInfo.searchFromName = null; + vm.searchInfo.results = []; + } + // method to select a search result + function selectResult(evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { + event: evt, + node: result + }); + } + //callback when there are search results + function onSearchResults(results) { + vm.searchInfo.results = results; + vm.searchInfo.showSearch = true; + } + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); + }); + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + function openMiniListView(node) { + $scope.miniListView = node; + } + } + angular.module('umbraco').controller('Umbraco.Overlays.MoveOverlay', MoveOverlay); + }()); + (function () { + 'use strict'; + function NodePermissionsController($scope, localizationService) { + var vm = this; + function onInit() { + // set default title + if (!$scope.model.title) { + localizationService.localize('defaultdialogs_permissionsEdit').then(function (value) { + $scope.model.title = value + ' ' + $scope.model.node.name; + }); } - else { - //it's an unhandled error - //if the service returns a detailed error - if (evt.InnerException) { - vm.zipFile.serverErrorMessage = evt.InnerException.ExceptionMessage; - - //Check if its the common "too large file" exception - if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { - vm.zipFile.serverErrorMessage = "File too large to upload"; + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Overlays.NodePermissionsController', NodePermissionsController); + }()); + (function () { + 'use strict'; + function QueryBuilderOverlayController($scope, templateQueryResource, localizationService) { + var everything = ''; + var myWebsite = ''; + var ascendingTranslation = ''; + var descendingTranslation = ''; + var vm = this; + vm.properties = []; + vm.contentTypes = []; + vm.conditions = []; + vm.datePickerConfig = { + pickDate: true, + pickTime: false, + format: 'YYYY-MM-DD' + }; + vm.chooseSource = chooseSource; + vm.getPropertyOperators = getPropertyOperators; + vm.addFilter = addFilter; + vm.trashFilter = trashFilter; + vm.changeSortOrder = changeSortOrder; + vm.setSortProperty = setSortProperty; + vm.setContentType = setContentType; + vm.setFilterProperty = setFilterProperty; + vm.setFilterTerm = setFilterTerm; + vm.changeConstraintValue = changeConstraintValue; + vm.datePickerChange = datePickerChange; + function onInit() { + vm.query = { + contentType: { name: everything }, + source: { name: myWebsite }, + filters: [{ + property: undefined, + operator: undefined + }], + sort: { + property: { + alias: '', + name: '' + }, + direction: 'ascending', + //This is the value for sorting sent to server + translation: { + currentLabel: ascendingTranslation, + //This is the localized UI value in the the dialog + ascending: ascendingTranslation, + descending: descendingTranslation } - - } else if (evt.Message) { - file.serverErrorMessage = evt.Message; } - } - }); - } - - function loadPackage() { - if (vm.zipFile.uploadStatus === "done") { - vm.state = "packageDetails"; - } - } - - function installPackage() { - vm.installState.status = localizationService.localize("packager_installStateImporting"); - vm.installState.progress = "0"; - - packageResource - .import(vm.localPackage) - .then(function(pack) { - vm.installState.progress = "25"; - vm.installState.status = localizationService.localize("packager_installStateInstalling"); - vm.installState.progress = "50"; - return packageResource.installFiles(pack); - }, - installError) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateRestarting"); - vm.installState.progress = "75"; - return packageResource.installData(pack); - }, - installError) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateComplete"); - vm.installState.progress = "100"; - return packageResource.cleanUp(pack); - }, - installError) - .then(function(result) { - - if (result.postInstallationPath) { - //Put the redirect Uri in a cookie so we can use after reloading - localStorageService.set("packageInstallUri", result.postInstallationPath); - } - else { - //set to a constant value so it knows to just go to the installed view - localStorageService.set("packageInstallUri", "installed"); + }; + templateQueryResource.getAllowedProperties().then(function (properties) { + vm.properties = properties; + }); + templateQueryResource.getContentTypes().then(function (contentTypes) { + vm.contentTypes = contentTypes; + }); + templateQueryResource.getFilterConditions().then(function (conditions) { + vm.conditions = conditions; + }); + throttledFunc(); + } + function chooseSource(query) { + vm.contentPickerOverlay = { + view: 'contentpicker', + show: true, + submit: function (model) { + var selectedNodeId = model.selection[0].id; + var selectedNodeName = model.selection[0].name; + if (selectedNodeId > 0) { + query.source = { + id: selectedNodeId, + name: selectedNodeName + }; + } else { + query.source.name = myWebsite; + delete query.source.id; } - - vm.installState.status = localizationService.localize("packager_installStateCompleted"); - vm.installCompleted = true; - - - + throttledFunc(); + vm.contentPickerOverlay.show = false; + vm.contentPickerOverlay = null; }, - installError); - } - - function installError() { - //This will return a rejection meaning that the promise change above will stop - return $q.reject(); - } - - vm.reloadPage = function() { - //reload on next digest (after cookie) - $timeout(function () { - $window.location.reload(true); - }); - } - } - - angular.module("umbraco").controller("Umbraco.Editors.Packages.InstallLocalController", PackagesInstallLocalController); - -})(); - -(function () { - "use strict"; - - function PackagesInstalledController($scope, $route, $location, packageResource, $timeout, $window, localStorageService, localizationService) { - - var vm = this; - - vm.confirmUninstall = confirmUninstall; - vm.uninstallPackage = uninstallPackage; - vm.state = "list"; - vm.installState = { - status: "" - }; - vm.package = {}; - - function init() { - packageResource.getInstalled() - .then(function (packs) { - vm.installedPackages = packs; - }); - vm.installState.status = ""; - vm.state = "list"; - } - - function confirmUninstall(pck) { - vm.state = "packageDetails"; - vm.package = pck; - } - - function uninstallPackage(installedPackage) { - vm.installState.status = localizationService.localize("packager_installStateUninstalling"); - vm.installState.progress = "0"; - - packageResource.uninstall(installedPackage.id) - .then(function () { - - if (installedPackage.files.length > 0) { - vm.installState.status = localizationService.localize("packager_installStateComplete"); - vm.installState.progress = "100"; - - //set this flag so that on refresh it shows the installed packages list - localStorageService.set("packageInstallUri", "installed"); - - //reload on next digest (after cookie) - $timeout(function () { - $window.location.reload(true); - }); - - } - else { - init(); + close: function (oldModel) { + vm.contentPickerOverlay.show = false; + vm.contentPickerOverlay = null; } + }; + } + function getPropertyOperators(property) { + var conditions = _.filter(vm.conditions, function (condition) { + var index = condition.appliesTo.indexOf(property.type); + return index >= 0; }); - } - - init(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.Packages.InstalledController", PackagesInstalledController); - -})(); - -(function () { - "use strict"; - - function PackagesRepoController($scope, $route, $location, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { - - var vm = this; - - vm.packageViewState = "packageList"; - vm.categories = []; - vm.loading = true; - vm.pagination = { - pageNumber: 1, - totalPages: 10, - pageSize: 24 - }; - vm.searchQuery = ""; - vm.installState = { - status: "", - progress: 0, - type: "ok" - }; - vm.selectCategory = selectCategory; - vm.showPackageDetails = showPackageDetails; - vm.setPackageViewState = setPackageViewState; - vm.nextPage = nextPage; - vm.prevPage = prevPage; - vm.goToPage = goToPage; - vm.installPackage = installPackage; - vm.downloadPackage = downloadPackage; - vm.openLightbox = openLightbox; - vm.closeLightbox = closeLightbox; - vm.search = search; - vm.installCompleted = false; - - var currSort = "Latest"; - //used to cancel any request in progress if another one needs to take it's place - var canceler = null; - - function getActiveCategory() { - if (vm.searchQuery !== "") { - return ""; + return conditions; + } + function addFilter(query) { + query.filters.push({}); } - for (var i = 0; i < vm.categories.length; i++) { - if (vm.categories[i].active === true) { - return vm.categories[i].name; + function trashFilter(query, filter) { + for (var i = 0; i < query.filters.length; i++) { + if (query.filters[i] == filter) { + query.filters.splice(i, 1); + } + } + //if we remove the last one, add a new one to generate ui for it. + if (query.filters.length == 0) { + query.filters.push({}); } } - return ""; - } - - function init() { - - vm.loading = true; - - $q.all([ - ourPackageRepositoryResource.getCategories() - .then(function(cats) { - vm.categories = cats; - }), - ourPackageRepositoryResource.getPopular(8) - .then(function(pack) { - vm.popular = pack.packages; - }), - ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort) - .then(function(pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - }) - ]) - .then(function() { - vm.loading = false; + function changeSortOrder(query) { + if (query.sort.direction === 'ascending') { + query.sort.direction = 'descending'; + query.sort.translation.currentLabel = query.sort.translation.descending; + } else { + query.sort.direction = 'ascending'; + query.sort.translation.currentLabel = query.sort.translation.ascending; + } + throttledFunc(); + } + function setSortProperty(query, property) { + query.sort.property = property; + if (property.type === 'datetime') { + query.sort.direction = 'descending'; + query.sort.translation.currentLabel = query.sort.translation.descending; + } else { + query.sort.direction = 'ascending'; + query.sort.translation.currentLabel = query.sort.translation.ascending; + } + throttledFunc(); + } + function setContentType(contentType) { + vm.query.contentType = contentType; + throttledFunc(); + } + function setFilterProperty(filter, property) { + filter.property = property; + filter.term = {}; + filter.constraintValue = ''; + } + function setFilterTerm(filter, term) { + filter.term = term; + if (filter.constraintValue) { + throttledFunc(); + } + } + function changeConstraintValue() { + throttledFunc(); + } + function datePickerChange(event, filter) { + if (event.date && event.date.isValid()) { + filter.constraintValue = event.date.format(vm.datePickerConfig.format); + throttledFunc(); + } + } + var throttledFunc = _.throttle(function () { + templateQueryResource.postTemplateQuery(vm.query).then(function (response) { + $scope.model.result = response; }); - + }, 200); + localizationService.localizeMany([ + 'template_allContent', + 'template_websiteRoot', + 'template_ascending', + 'template_descending' + ]).then(function (res) { + everything = res[0]; + myWebsite = res[1]; + ascendingTranslation = res[2]; + descendingTranslation = res[3]; + onInit(); + }); } - - function selectCategory(selectedCategory, categories) { - var reset = false; - for (var i = 0; i < categories.length; i++) { - var category = categories[i]; - if (category.name === selectedCategory.name && category.active === true) { - //it's already selected, let's unselect to show all again - reset = true; + angular.module('umbraco').controller('Umbraco.Overlays.QueryBuilderController', QueryBuilderOverlayController); + }()); + (function () { + 'use strict'; + function SectionPickerController($scope, sectionResource, localizationService) { + var vm = this; + vm.sections = []; + vm.loading = false; + vm.selectSection = selectSection; + ////////// + function onInit() { + vm.loading = true; + // set default title + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectSections'); } - category.active = false; - } - - vm.loading = true; - vm.searchQuery = ""; - var searchCategory = selectedCategory.name; - if (reset === true) { - searchCategory = ""; - } - - currSort = "Latest"; - - $q.all([ - ourPackageRepositoryResource.getPopular(8, searchCategory) - .then(function(pack) { - vm.popular = pack.packages; - }), - ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, searchCategory, vm.searchQuery) - .then(function(pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - vm.pagination.pageNumber = 1; - }) - ]) - .then(function() { + // make sure we can push to something + if (!$scope.model.selection) { + $scope.model.selection = []; + } + // get sections + sectionResource.getAllSections().then(function (sections) { + vm.sections = sections; + setSectionIcon(vm.sections); + if ($scope.model.selection && $scope.model.selection.length > 0) { + preSelect($scope.model.selection); + } vm.loading = false; - selectedCategory.active = reset === false; }); - } - - function showPackageDetails(selectedPackage) { - ourPackageRepositoryResource.getDetails(selectedPackage.id) - .then(function (pack) { - packageResource.validateInstalled(pack.name, pack.latestVersion) - .then(function() { - //ok, can install - vm.package = pack; - vm.package.isValid = true; - vm.packageViewState = "packageDetails"; - }, function() { - //nope, cannot install - vm.package = pack; - vm.package.isValid = false; - vm.packageViewState = "packageDetails"; - }) - }); - } - - function setPackageViewState(state) { - if(state) { - vm.packageViewState = state; - } - } - - function nextPage(pageNumber) { - ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) - .then(function (pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + } + function preSelect(selection) { + angular.forEach(selection, function (selected) { + angular.forEach(vm.sections, function (section) { + if (selected.alias === section.alias) { + section.selected = true; + } + }); }); - } - - function prevPage(pageNumber) { - ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) - .then(function (pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + } + function selectSection(section) { + if (!section.selected) { + section.selected = true; + $scope.model.selection.push(section); + } else { + angular.forEach($scope.model.selection, function (selectedSection, index) { + if (selectedSection.alias === section.alias) { + section.selected = false; + $scope.model.selection.splice(index, 1); + } + }); + } + } + function setSectionIcon(sections) { + angular.forEach(sections, function (section) { + section.icon = 'icon-section ' + section.cssclass; }); + } + onInit(); } - - function goToPage(pageNumber) { - ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) - .then(function (pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - }); + angular.module('umbraco').controller('Umbraco.Overlays.SectionPickerController', SectionPickerController); + }()); + (function () { + 'use strict'; + function TemplateSectionsOverlayController($scope) { + var vm = this; + $scope.model.mandatoryRenderSection = false; + if (!$scope.model.title) { + $scope.model.title = 'Sections'; + } + vm.select = select; + function onInit() { + if ($scope.model.hasMaster) { + $scope.model.insertType = 'addSection'; + } else { + $scope.model.insertType = 'renderBody'; + } + } + function select(type) { + $scope.model.insertType = type; + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Overlays.TemplateSectionsOverlay', TemplateSectionsOverlayController); + }()); + //used for the media picker dialog + angular.module('umbraco').controller('Umbraco.Overlays.TreePickerController', function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { + var tree = null; + var dialogOptions = $scope.model; + $scope.treeReady = false; + $scope.dialogTreeEventHandler = $({}); + $scope.section = dialogOptions.section; + $scope.treeAlias = dialogOptions.treeAlias; + $scope.multiPicker = dialogOptions.multiPicker; + $scope.hideHeader = typeof dialogOptions.hideHeader === 'boolean' ? dialogOptions.hideHeader : true; + // if you need to load a not initialized tree set this value to false - default is true + $scope.onlyInitialized = dialogOptions.onlyInitialized; + $scope.searchInfo = { + searchFromId: dialogOptions.startNodeId, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + $scope.model.selection = []; + //Used for toggling an empty-state message + //Some trees can have no items (dictionary & forms email templates) + $scope.hasItems = true; + $scope.emptyStateMessage = dialogOptions.emptyStateMessage; + //This is called from ng-init + //it turns out it is called from the angular html : / Have a look at views/common / overlays / contentpicker / contentpicker.html you'll see ng-init. + //this is probably an anti pattern IMO and shouldn't be used + $scope.init = function (contentType) { + if (contentType === 'content') { + $scope.entityType = 'Document'; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectContent'); + } + } else if (contentType === 'member') { + $scope.entityType = 'Member'; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectMember'); + } + } else if (contentType === 'media') { + $scope.entityType = 'Media'; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectMedia'); + } + } + }; + var searchText = 'Search...'; + localizationService.localize('general_search').then(function (value) { + searchText = value + '...'; + }); + // Allow the entity type to be passed in but defaults to Document for backwards compatibility. + $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : 'Document'; + //min / max values + if (dialogOptions.minNumber) { + dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); + } + if (dialogOptions.maxNumber) { + dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); + } + if (dialogOptions.section === 'member') { + $scope.entityType = 'Member'; + } else if (dialogOptions.section === 'media') { + $scope.entityType = 'Media'; + } + // Search and listviews is only working for content, media and member section + var searchableSections = [ + 'content', + 'media', + 'member' + ]; + $scope.enableSearh = searchableSections.indexOf($scope.section) !== -1; + //if a alternative startnode is used, we need to check if it is a container + if ($scope.enableSearh && dialogOptions.startNodeId && dialogOptions.startNodeId !== -1 && dialogOptions.startNodeId !== '-1') { + entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function (node) { + if (node.metaData.IsContainer) { + openMiniListView(node); + } + initTree(); + }); + } else { + initTree(); + } + //Configures filtering + if (dialogOptions.filter) { + dialogOptions.filterExclude = false; + dialogOptions.filterAdvanced = false; + //used advanced filtering + if (angular.isFunction(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } else if (angular.isObject(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } else { + if (dialogOptions.filter.startsWith('!')) { + dialogOptions.filterExclude = true; + dialogOptions.filter = dialogOptions.filter.substring(1); + } + //used advanced filtering + if (dialogOptions.filter.startsWith('{')) { + dialogOptions.filterAdvanced = true; + //convert to object + dialogOptions.filter = angular.fromJson(dialogOptions.filter); + } + } } - - function downloadPackage(selectedPackage) { - vm.loading = true; - - packageResource - .fetch(selectedPackage.id) - .then(function(pack) { - vm.packageViewState = "packageInstall"; - vm.loading = false; - vm.localPackage = pack; - vm.localPackage.allowed = true; - }, function (evt, status, headers, config) { - - if (status == 400) { - //it's a validation error - vm.installState.type = "error"; - vm.zipFile.serverErrorMessage = evt.message; + function initTree() { + //create the custom query string param for this tree + $scope.customTreeParams = dialogOptions.startNodeId ? 'startNodeId=' + dialogOptions.startNodeId : ''; + $scope.customTreeParams += dialogOptions.customTreeParams ? '&' + dialogOptions.customTreeParams : ''; + $scope.treeReady = true; + } + function nodeExpandedHandler(ev, args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + if (angular.isArray(args.children)) { + //iterate children + _.each(args.children, function (child) { + //now we need to look in the already selected search results and + // toggle the check boxes for those ones that are listed + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + if (exists) { + child.selected = true; } }); + //check filter + performFiltering(args.children); + } } - - function error(e, args) { - //This will return a rejection meaning that the promise change above will stop - return $q.reject(); - } - - function installPackage(selectedPackage) { - - vm.installState.status = localizationService.localize("packager_installStateImporting"); - vm.installState.progress = "0"; - - packageResource - .import(selectedPackage) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateInstalling"); - vm.installState.progress = "33"; - return packageResource.installFiles(pack); - }, - error) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateRestarting"); - vm.installState.progress = "66"; - return packageResource.installData(pack); - }, - error) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateComplete"); - vm.installState.progress = "100"; - return packageResource.cleanUp(pack); - }, - error) - .then(function(result) { - - if (result.postInstallationPath) { - //Put the redirect Uri in a cookie so we can use after reloading - localStorageService.set("packageInstallUri", result.postInstallationPath); - } - - vm.installState.status = localizationService.localize("packager_installStateCompleted"); - vm.installCompleted = true; - - }, - error); + //gets the tree object when it loads + function treeLoadedHandler(ev, args) { + //args.tree contains children (args.tree.root.children) + $scope.hasItems = args.tree.root.children.length > 0; + tree = args.tree; } - - function openLightbox(itemIndex, items) { - vm.lightbox = { - show: true, - items: items, - activeIndex: itemIndex - }; + //wires up selection + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + if (args.node.metaData.isSearchResult) { + //check if the item selected was a search result from a list view + //unselect + select(args.node.name, args.node.id); + //remove it from the list view children + var listView = args.node.parent(); + listView.children = _.reject(listView.children, function (child) { + return child.id == args.node.id; + }); + //remove it from the custom tracked search result list + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { + return i.id == args.node.id; + }); + } else { + eventsService.emit('dialogs.treePickerController.select', args); + if (args.node.filtered) { + return; + } + //This is a tree node, so we don't have an entity to pass in, it will need to be looked up + //from the server in this method. + if ($scope.model.select) { + $scope.model.select(args.node); + } else { + select(args.node.name, args.node.id); + //toggle checked state + args.node.selected = args.node.selected === true ? false : true; + } + } } - - function closeLightbox() { - vm.lightbox.show = false; - vm.lightbox = null; + /** Method used for selecting a node */ + function select(text, id, entity) { + //if we get the root, we just return a constructed entity, no need for server data + if (id < 0) { + var rootNode = { + alias: null, + icon: 'icon-folder', + id: id, + name: text + }; + if ($scope.multiPicker) { + if (entity) { + multiSelectItem(entity); + } else { + multiSelectItem(rootNode); + } + } else { + $scope.model.selection.push(rootNode); + $scope.model.submit($scope.model); + } + } else { + if ($scope.multiPicker) { + if (entity) { + multiSelectItem(entity); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, $scope.entityType).then(function (ent) { + multiSelectItem(ent); + }); + } + } else { + $scope.hideSearch(); + //if an entity has been passed in, use it + if (entity) { + $scope.model.selection.push(entity); + $scope.model.submit($scope.model); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, $scope.entityType).then(function (ent) { + $scope.model.selection.push(ent); + $scope.model.submit($scope.model); + }); + } + } + } } - - - var searchDebounced = _.debounce(function(e) { - - $scope.$apply(function () { - - //a canceler exists, so perform the cancelation operation and reset - if (canceler) { - canceler.resolve(); - canceler = $q.defer(); - } - else { - canceler = $q.defer(); - } - - currSort = vm.searchQuery ? "Default" : "Latest"; - - ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, - vm.pagination.pageSize, - currSort, - "", - vm.searchQuery, - canceler) - .then(function(pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - vm.pagination.pageNumber = 1; - vm.loading = false; - //set back to null so it can be re-created - canceler = null; - }); - - }); - - }, 200); - - function search(searchQuery) { - vm.loading = true; - searchDebounced(); + function multiSelectItem(item) { + var found = false; + var foundIndex = 0; + if ($scope.model.selection.length > 0) { + for (i = 0; $scope.model.selection.length > i; i++) { + var selectedItem = $scope.model.selection[i]; + if (selectedItem.id === item.id) { + found = true; + foundIndex = i; + } + } + } + if (found) { + $scope.model.selection.splice(foundIndex, 1); + } else { + $scope.model.selection.push(item); + } } - - vm.reloadPage = function () { - //reload on next digest (after cookie) - $timeout(function () { - window.location.reload(true); + function performFiltering(nodes) { + if (!dialogOptions.filter) { + return; + } + //remove any list view search nodes from being filtered since these are special nodes that always must + // be allowed to be clicked on + nodes = _.filter(nodes, function (n) { + return !angular.isObject(n.metaData.listViewNode); }); - } - - init(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.Packages.RepoController", PackagesRepoController); - -})(); - -(function () { - "use strict"; - - function PartialViewMacrosCreateController($scope, codefileResource, macroResource, $location, navigationService, formHelper, localizationService, appState) { - - var vm = this; - var node = $scope.dialogOptions.currentNode; - var localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); - - vm.snippets = []; - vm.createFolderError = ""; - vm.folderName = ""; - vm.fileName = ""; - vm.showSnippets = false; - vm.creatingFolder = false; - - vm.showCreateFolder = showCreateFolder; - vm.createFolder = createFolder; - vm.createFile = createFile; - vm.createFileWithoutMacro = createFileWithoutMacro; - vm.showCreateFromSnippet = showCreateFromSnippet; - vm.createFileFromSnippet = createFileFromSnippet; - - function onInit() { - codefileResource.getSnippets('partialViewMacros') - .then(function (snippets) { - vm.snippets = snippets; + if (dialogOptions.filterAdvanced) { + //filter either based on a method or an object + var filtered = angular.isFunction(dialogOptions.filter) ? _.filter(nodes, dialogOptions.filter) : _.where(nodes, dialogOptions.filter); + angular.forEach(filtered, function (value, key) { + value.filtered = true; + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); + } }); - } - - function showCreateFolder() { - vm.creatingFolder = true; - } - - function createFolder(form) { - if (formHelper.submitForm({ scope: $scope, formCtrl: form, statusMessage: localizeCreateFolder })) { - - codefileResource.createContainer("partialViewMacros", node.id, vm.folderName).then(function (saved) { - - navigationService.hideMenu(); - - navigationService.syncTree({ - tree: "partialViewMacros", - path: saved.path, - forceReload: true, - activate: true - }); - - formHelper.resetForm({ - scope: $scope - }); - - var section = appState.getSectionState("currentSection"); - - }, function (err) { - - vm.createFolderError = err; - - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); + } else { + var a = dialogOptions.filter.toLowerCase().replace(/\s/g, '').split(','); + angular.forEach(nodes, function (value, key) { + var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; + if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { + value.filtered = true; + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); } } }); } } - - function createFile() { - $location.path("/developer/partialviewmacros/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - } - - function createFileWithoutMacro() { - $location.path("/developer/partialviewmacros/edit/" + node.id).search("create", "true").search("nomacro", "true"); - navigationService.hideMenu(); - } - - function createFileFromSnippet(snippet) { - $location.path("/developer/partialviewmacros/edit/" + node.id).search("create", "true").search("snippet", snippet.fileName); - navigationService.hideMenu(); - } - - function showCreateFromSnippet() { - vm.showSnippets = true; - } - - onInit(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.PartialViewMacros.CreateController", PartialViewMacrosCreateController); -})(); - -/** - * @ngdoc controller - * @name Umbraco.Editors.PartialViewMacros.DeleteController - * @function - * - * @description - * The controller for deleting partial view macros - */ -function PartialViewMacrosDeleteController($scope, codefileResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - - codefileResource.deleteByPath('partialViewMacros', $scope.currentNode.id) - .then(function() { - $scope.currentNode.loading = false; - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); + $scope.multiSubmit = function (result) { + entityResource.getByIds(result, $scope.entityType).then(function (ents) { + $scope.submit(ents); }); - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.PartialViewMacros.DeleteController", PartialViewMacrosDeleteController); - -(function () { - "use strict"; - - function partialViewMacrosEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, macroResource) { - - var vm = this; - var localizeSaving = localizationService.localize("general_saving"); - - vm.page = {}; - vm.page.loading = true; - vm.partialViewMacroFile = {}; - - //menu - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; - - // bind functions to view model - vm.save = save; - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openQueryBuilderOverlay = openQueryBuilderOverlay; - vm.openMacroOverlay = openMacroOverlay; - vm.openInsertOverlay = openInsertOverlay; - - /* Functions bound to view model */ - - function save() { - - vm.page.saveButtonState = "busy"; - vm.partialViewMacro.content = vm.editor.getValue(); - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: localizeSaving, - saveMethod: codefileResource.save, - scope: $scope, - content: vm.partialViewMacro, - // We do not redirect on failure for partial view macros - this is because it is not possible to actually save the partial view - // when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - rebindCallback: function (orignal, saved) {} - }).then(function (saved) { - // create macro if needed - if($routeParams.create && $routeParams.nomacro !== "true") { - macroResource.createPartialViewMacroWithFile(saved.virtualPath, saved.name).then(function(created) { - completeSave(saved); - }, function(err) { - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } + }; + /** method to select a search result */ + $scope.selectResult = function (evt, result) { + if (result.filtered) { + return; + } + result.selected = result.selected === true ? false : true; + //since result = an entity, we'll pass it in so we don't have to go back to the server + select(result.name, result.id, result); + //add/remove to our custom tracked list of selected search results + if (result.selected) { + $scope.searchInfo.selectedSearchResults.push(result); + } else { + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { + return i.id == result.id; + }); + } + //ensure the tree node in the tree is checked/unchecked if it already exists there + if (tree) { + var found = treeService.getDescendantNode(tree.root, result.id); + if (found) { + found.selected = result.selected; + } + } + }; + $scope.hideSearch = function () { + //Traverse the entire displayed tree and update each node to sync with the selected search results + if (tree) { + //we need to ensure that any currently displayed nodes that get selected + // from the search get updated to have a check box! + function checkChildren(children) { + _.each(children, function (child) { + //check if the id is in the selection, if so ensure it's flagged as selected + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + //if the curr node exists in selected search results, ensure it's checked + if (exists) { + child.selected = true; + } //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result + else if (child.metaData.isSearchResult) { + //if this tree node is under a list view it means that the node was added + // to the tree dynamically under the list view that was searched, so we actually want to remove + // it all together from the tree + var listView = child.parent(); + listView.children = _.reject(listView.children, function (c) { + return c.id == child.id; + }); + } + //check if the current node is a list view and if so, check if there's any new results + // that need to be added as child nodes to it based on search results selected + if (child.metaData.isContainer) { + child.cssClasses = _.reject(child.cssClasses, function (c) { + return c === 'tree-node-slide-up-hide-active'; + }); + var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { + return i.parentId == child.id; + }); + _.each(listViewResults, function (item) { + var childExists = _.find(child.children, function (c) { + return c.id == item.id; + }); + if (!childExists) { + var parent = child; + child.children.unshift({ + id: item.id, + name: item.name, + cssClass: 'icon umb-tree-icon sprTree ' + item.icon, + level: child.level + 1, + metaData: { isSearchResult: true }, + hasChildren: false, + parent: function () { + return parent; + } + }); + } + }); + } + //recurse + if (child.children && child.children.length > 0) { + checkChildren(child.children); } }); - } else { - completeSave(saved); } - - }, function (err) { - - vm.page.saveButtonState = "error"; - - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); - - }); - - } - - function completeSave(saved) { - - localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_partialViewSavedText").then(function (msgValue) { - notificationsService.success(headerValue, msgValue); - }); + checkChildren(tree.root.children); + } + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = dialogOptions.startNodeId; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + }; + $scope.onSearchResults = function (results) { + //filter all items - this will mark an item as filtered + performFiltering(results); + //now actually remove all filtered items so they are not even displayed + results = _.filter(results, function (item) { + return !item.filtered; }); - - //check if the name changed, if so we need to redirect - if (vm.partialViewMacro.id !== saved.id) { - contentEditingHelper.redirectToRenamedContent(saved.id); - } - else { - vm.page.saveButtonState = "success"; - vm.partialViewMacro = saved; - - //sync state - editorState.set(vm.partialViewMacro); - - // normal tree sync - navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; + $scope.searchInfo.results = results; + //sync with the curr selected results + _.each($scope.searchInfo.results, function (result) { + var exists = _.find($scope.model.selection, function (selectedId) { + return result.id == selectedId; }); - - // clear $dirty state on form - setFormState("pristine"); - } - - } - - function openInsertOverlay() { - - vm.insertOverlay = { - view: "insert", - allowedTypes: { - macro: true, - dictionary: true, - umbracoField: true - }, - hideSubmitButton: true, - show: true, - submit: function(model) { - - switch(model.insert.type) { - case "macro": - var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); - insert(macroObject.syntax); - break; - - case "dictionary": - var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); - insert(code); - break; - - case "umbracoField": - insert(model.insert.umbracoField); - break; - } - - vm.insertOverlay.show = false; - vm.insertOverlay = null; - - }, - close: function(oldModel) { - // close the dialog - vm.insertOverlay.show = false; - vm.insertOverlay = null; - // focus editor - vm.editor.focus(); + if (exists) { + result.selected = true; } - }; - - } - - - function openMacroOverlay() { - - vm.macroPickerOverlay = { - view: "macropicker", - dialogData: {}, - show: true, - title: "Insert macro", - submit: function (model) { - - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); - insert(macroObject.syntax); - - vm.macroPickerOverlay.show = false; - vm.macroPickerOverlay = null; - - }, - close: function(oldModel) { - // close the dialog - vm.macroPickerOverlay.show = false; - vm.macroPickerOverlay = null; - // focus editor - vm.editor.focus(); - } - }; + }); + $scope.searchInfo.showSearch = true; + }; + $scope.dialogTreeEventHandler.bind('treeLoaded', treeLoadedHandler); + $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeLoaded', treeLoadedHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + }); + $scope.selectListViewNode = function (node) { + select(node.name, node.id); + //toggle checked state + node.selected = node.selected === true ? false : true; + }; + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + function openMiniListView(node) { + $scope.miniListView = node; } - - - function openPageFieldOverlay() { - vm.pageFieldOverlay = { - submitButtonLabel: "Insert", - closeButtonlabel: "Cancel", - view: "insertfield", - show: true, - submit: function (model) { - insert(model.umbracoField); - vm.pageFieldOverlay.show = false; - vm.pageFieldOverlay = null; - }, - close: function (model) { - // close the dialog - vm.pageFieldOverlay.show = false; - vm.pageFieldOverlay = null; - // focus editor - vm.editor.focus(); - } - }; + }); + angular.module('umbraco').controller('Umbraco.Overlays.UserController', function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper, localizationService) { + $scope.history = historyService.getCurrent(); + $scope.version = Umbraco.Sys.ServerVariables.application.version + ' assembly: ' + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.showPasswordFields = false; + $scope.changePasswordButtonState = 'init'; + $scope.model.subtitle = 'Umbraco version' + ' ' + $scope.version; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('general_user'); } - - - function openDictionaryItemOverlay() { - vm.dictionaryItemOverlay = { - view: "treepicker", - section: "settings", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - show: true, - title: "Insert dictionary item", - emptyStateMessage: localizationService.localize("emptyStates_emptyDictionaryTree"), - select: function(node){ - - var code = templateHelper.getInsertDictionarySnippet(node.name); - insert(code); - - vm.dictionaryItemOverlay.show = false; - vm.dictionaryItemOverlay = null; - }, - close: function (model) { - // close dialog - vm.dictionaryItemOverlay.show = false; - vm.dictionaryItemOverlay = null; - // focus editor - vm.editor.focus(); + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; + var evts = []; + evts.push(eventsService.on('historyService.add', function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on('historyService.remove', function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on('historyService.removeAll', function (e, args) { + $scope.history = []; + })); + $scope.logout = function () { + //Add event listener for when there are pending changes on an editor which means our route was not successful + var pendingChangeEvent = eventsService.on('valFormManager.pendingChanges', function (e, args) { + //one time listener, remove the event + pendingChangeEvent(); + $scope.model.close(); + }); + //perform the path change, if it is successful then the promise will resolve otherwise it will fail + $scope.model.close(); + $location.path('/logout'); + }; + $scope.gotoHistory = function (link) { + $location.path(link); + $scope.model.close(); + }; + //Manually update the remaining timeout seconds + function updateTimeout() { + $timeout(function () { + if ($scope.remainingAuthSeconds > 0) { + $scope.remainingAuthSeconds--; + $scope.$digest(); + //recurse + updateTimeout(); } - }; + }, 1000, false); // 1 second, do NOT execute a global digest } - - function openQueryBuilderOverlay() { - vm.queryBuilderOverlay = { - view: "querybuilder", - show: true, - title: "Query for content", - - submit: function (model) { - - var code = templateHelper.getQuerySnippet(model.result.queryExpression); - insert(code); - - vm.queryBuilderOverlay.show = false; - vm.queryBuilderOverlay = null; - }, - - close: function (model) { - // close dialog - vm.queryBuilderOverlay.show = false; - vm.queryBuilderOverlay = null; - // focus editor - vm.editor.focus(); + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + $scope.user = user; + if ($scope.user) { + $scope.model.title = user.name; + $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; + $scope.canEditProfile = _.indexOf($scope.user.allowedSections, 'users') > -1; + //set the timer + updateTimeout(); + authResource.getCurrentUserLinkedLogins().then(function (logins) { + //reset all to be un-linked + for (var provider in $scope.externalLoginProviders) { + $scope.externalLoginProviders[provider].linkedProviderKey = undefined; + } + //set the linked logins + for (var login in logins) { + var found = _.find($scope.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); } - }; + }); } - - /* Local functions */ - - function init() { - //we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css"); - - if ($routeParams.create) { - - var snippet = "Empty"; - - if($routeParams.snippet) { - snippet = $routeParams.snippet; - } - - codefileResource.getScaffold("partialViewMacros", $routeParams.id, snippet).then(function (partialViewMacro) { - if ($routeParams.name) { - partialViewMacro.name = $routeParams.name; - } - ready(partialViewMacro, false); - }); - - } else { - codefileResource.getByPath('partialViewMacros', $routeParams.id).then(function (partialViewMacro) { - ready(partialViewMacro, true); - }); - } - } - - function ready(partialViewMacro, syncTree) { - - vm.page.loading = false; - vm.partialViewMacro = partialViewMacro; - - //sync state - editorState.set(vm.partialViewMacro); - - if (syncTree) { - navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); + $scope.unlink = function (e, loginProvider, providerKey) { + var result = confirm('Are you sure you want to unlink this account?'); + if (!result) { + e.preventDefault(); + return; } - - // ace configuration - vm.aceOption = { - mode: "razor", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px' - }, - onLoad: function(_editor) { - vm.editor = _editor; - - // initial cursor placement - // Keep cursor in name field if we are create a new template - // else set the cursor at the bottom of the code editor - if(!$routeParams.create) { - $timeout(function(){ - vm.editor.navigateFileEnd(); - vm.editor.focus(); - persistCurrentLocation(); - }); + authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { + updateUserInfo(); + }); + }; + updateUserInfo(); + //remove all event handlers + $scope.$on('$destroy', function () { + for (var e = 0; e < evts.length; e++) { + evts[e](); + } + }); + /* ---------- UPDATE PASSWORD ---------- */ + //create the initial model for change password + $scope.changePasswordModel = { + config: {}, + value: {} + }; + //go get the config for the membership provider and add it to the model + authResource.getMembershipProviderConfig().then(function (data) { + $scope.changePasswordModel.config = data; + //ensure the hasPassword config option is set to true (the user of course has a password already assigned) + //this will ensure the oldPassword is shown so they can change it + // disable reset password functionality beacuse it does not make sense inside the backoffice + $scope.changePasswordModel.config.hasPassword = true; + $scope.changePasswordModel.config.disableToggle = true; + $scope.changePasswordModel.config.enableReset = false; + }); + $scope.changePassword = function () { + if (formHelper.submitForm({ scope: $scope })) { + $scope.changePasswordButtonState = 'busy'; + currentUserResource.changePassword($scope.changePasswordModel.value).then(function (data) { + //reset old data + clearPasswordFields(); + //if the password has been reset, then update our model + if (data.value) { + $scope.changePasswordModel.value.generatedPassword = data.value; } - - //change on blur, focus - vm.editor.on("blur", persistCurrentLocation); - vm.editor.on("focus", persistCurrentLocation); - vm.editor.on("change", changeAceEditor); - - } - } - - } - - function insert(str) { - vm.editor.focus(); - vm.editor.moveCursorToPosition(vm.currentPosition); - vm.editor.insert(str); - - // set form state to $dirty - setFormState("dirty"); - } - - function persistCurrentLocation() { - vm.currentPosition = vm.editor.getCursorPosition(); - } - - function changeAceEditor() { - setFormState("dirty"); - } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if(state === "dirty") { - currentForm.$setDirty(); - } else if(state === "pristine") { - currentForm.$setPristine(); + formHelper.resetForm({ + scope: $scope, + notifications: data.notifications + }); + $scope.changePasswordButtonState = 'success'; + $timeout(function () { + $scope.togglePasswordFields(); + }, 2000); + }, function (err) { + formHelper.handleError(err); + $scope.changePasswordButtonState = 'error'; + }); } + }; + $scope.togglePasswordFields = function () { + clearPasswordFields(); + $scope.showPasswordFields = !$scope.showPasswordFields; + }; + function clearPasswordFields() { + $scope.changePasswordModel.value.oldPassword = ''; + $scope.changePasswordModel.value.newPassword = ''; + $scope.changePasswordModel.value.confirm = ''; } - - - init(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.PartialViewMacros.EditController", partialViewMacrosEditController); -})(); -(function () { - "use strict"; - - function PartialViewsCreateController($scope, codefileResource, $location, navigationService, formHelper, localizationService, appState) { - - var vm = this; - var node = $scope.dialogOptions.currentNode; - var localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); - - vm.snippets = []; - vm.showSnippets = false; - vm.creatingFolder = false; - vm.createFolderError = ""; - vm.folderName = ""; - - vm.createPartialView = createPartialView; - vm.showCreateFolder = showCreateFolder; - vm.createFolder = createFolder; - vm.showCreateFromSnippet = showCreateFromSnippet; - - function onInit() { - codefileResource.getSnippets('partialViews') - .then(function(snippets) { - vm.snippets = snippets; + }); + (function () { + 'use strict'; + function UserGroupPickerController($scope, userGroupsResource, localizationService) { + var vm = this; + vm.userGroups = []; + vm.loading = false; + vm.selectUserGroup = selectUserGroup; + ////////// + function onInit() { + vm.loading = true; + // set default title + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectUsers'); + } + // make sure we can push to something + if (!$scope.model.selection) { + $scope.model.selection = []; + } + // get venues + userGroupsResource.getUserGroups().then(function (userGroups) { + vm.userGroups = userGroups; + if ($scope.model.selection && $scope.model.selection.length > 0) { + preSelect($scope.model.selection); + } + vm.loading = false; }); - } - - function createPartialView(selectedSnippet) { - - var snippet = null; - - if(selectedSnippet && selectedSnippet.fileName) { - snippet = selectedSnippet.fileName; } - - $location.path("/settings/partialviews/edit/" + node.id).search("create", "true").search("snippet", snippet); - navigationService.hideMenu(); - - } - - function showCreateFolder() { - vm.creatingFolder = true; - } - - function createFolder(form) { - if (formHelper.submitForm({scope: $scope, formCtrl: form, statusMessage: localizeCreateFolder})) { - - codefileResource.createContainer("partialViews", node.id, vm.folderName).then(function(saved) { - - navigationService.hideMenu(); - - navigationService.syncTree({ - tree: "partialViews", - path: saved.path, - forceReload: true, - activate: true - }); - - formHelper.resetForm({ - scope: $scope - }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - vm.createFolderError = err; - - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); + function preSelect(selection) { + angular.forEach(selection, function (selected) { + angular.forEach(vm.userGroups, function (userGroup) { + if (selected.id === userGroup.id) { + userGroup.selected = true; } - } + }); }); } - } - - function showCreateFromSnippet() { - vm.showSnippets = true; - } - - onInit(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.PartialViews.CreateController", PartialViewsCreateController); -})(); - -/** - * @ngdoc controller - * @name Umbraco.Editors.PartialViews.DeleteController - * @function - * - * @description - * The controller for deleting partial views - */ -function PartialViewsDeleteController($scope, codefileResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - - codefileResource.deleteByPath('partialViews', $scope.currentNode.id) - .then(function() { - $scope.currentNode.loading = false; - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.PartialViews.DeleteController", PartialViewsDeleteController); - -(function () { - "use strict"; - - function PartialViewsEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper) { - - var vm = this; - var localizeSaving = localizationService.localize("general_saving"); - - vm.page = {}; - vm.page.loading = true; - vm.partialView = {}; - - //menu - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; - - //Used to toggle the keyboard shortcut modal - //From a custom keybinding in ace editor - that conflicts with our own to show the dialog - vm.showKeyboardShortcut = false; - - //Keyboard shortcuts for help dialog - vm.page.keyboardShortcutsOverview = []; - vm.page.keyboardShortcutsOverview.push(templateHelper.getGeneralShortcuts()); - vm.page.keyboardShortcutsOverview.push(templateHelper.getEditorShortcuts()); - vm.page.keyboardShortcutsOverview.push(templateHelper.getPartialViewEditorShortcuts()); - - // bind functions to view model - vm.save = save; - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openQueryBuilderOverlay = openQueryBuilderOverlay; - vm.openMacroOverlay = openMacroOverlay; - vm.openInsertOverlay = openInsertOverlay; - - /* Functions bound to view model */ - - function save() { - - vm.page.saveButtonState = "busy"; - vm.partialView.content = vm.editor.getValue(); - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: localizeSaving, - saveMethod: codefileResource.save, - scope: $scope, - content: vm.partialView, - //We do not redirect on failure for partialviews - this is because it is not possible to actually save the partialviews - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - rebindCallback: function (orignal, saved) {} - }).then(function (saved) { - - localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_partialViewSavedText").then(function(msgValue) { - notificationsService.success(headerValue, msgValue); + function selectUserGroup(userGroup) { + if (!userGroup.selected) { + userGroup.selected = true; + $scope.model.selection.push(userGroup); + } else { + angular.forEach($scope.model.selection, function (selectedUserGroup, index) { + if (selectedUserGroup.id === userGroup.id) { + userGroup.selected = false; + $scope.model.selection.splice(index, 1); + } }); - }); - - //check if the name changed, if so we need to redirect - if (vm.partialView.id !== saved.id) { - contentEditingHelper.redirectToRenamedContent(saved.id); } - else { - vm.page.saveButtonState = "success"; - vm.partialView = saved; - - //sync state - editorState.set(vm.partialView); - - // normal tree sync - navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - - // clear $dirty state on form - setFormState("pristine"); + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Overlays.UserGroupPickerController', UserGroupPickerController); + }()); + (function () { + 'use strict'; + function UserPickerController($scope, usersResource, localizationService) { + var vm = this; + vm.users = []; + vm.loading = false; + vm.usersOptions = {}; + vm.selectUser = selectUser; + vm.searchUsers = searchUsers; + vm.changePageNumber = changePageNumber; + ////////// + function onInit() { + vm.loading = true; + // set default title + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('defaultdialogs_selectUsers'); } - }, function (err) { - - vm.page.saveButtonState = "error"; - - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { - notificationsService.error(headerValue, msgValue); + // make sure we can push to something + if (!$scope.model.selection) { + $scope.model.selection = []; + } + // get users + getUsers(); + } + function preSelect(selection, users) { + angular.forEach(selection, function (selected) { + angular.forEach(users, function (user) { + if (selected.id === user.id) { + user.selected = true; + } }); }); - - }); - - } - - function openInsertOverlay() { - - vm.insertOverlay = { - view: "insert", - allowedTypes: { - macro: true, - dictionary: true, - umbracoField: true - }, - hideSubmitButton: true, - show: true, - submit: function(model) { - - switch(model.insert.type) { - case "macro": - var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); - insert(macroObject.syntax); - break; - - case "dictionary": - var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); - insert(code); - break; - - case "umbracoField": - insert(model.insert.umbracoField); - break; - } - - vm.insertOverlay.show = false; - vm.insertOverlay = null; - - }, - close: function(oldModel) { - // close the dialog - vm.insertOverlay.show = false; - vm.insertOverlay = null; - // focus editor - vm.editor.focus(); + } + function selectUser(user) { + if (!user.selected) { + user.selected = true; + $scope.model.selection.push(user); + } else { + angular.forEach($scope.model.selection, function (selectedUser, index) { + if (selectedUser.id === user.id) { + user.selected = false; + $scope.model.selection.splice(index, 1); + } + }); } - }; - - } - - - function openMacroOverlay() { - - vm.macroPickerOverlay = { - view: "macropicker", - dialogData: {}, - show: true, - title: "Insert macro", - submit: function (model) { - - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); - insert(macroObject.syntax); - - vm.macroPickerOverlay.show = false; - vm.macroPickerOverlay = null; - - }, - close: function(oldModel) { - // close the dialog - vm.macroPickerOverlay.show = false; - vm.macroPickerOverlay = null; - // focus editor - vm.editor.focus(); - } - }; - } - - - function openPageFieldOverlay() { - vm.pageFieldOverlay = { - submitButtonLabel: "Insert", - closeButtonlabel: "Cancel", - view: "insertfield", - show: true, - submit: function (model) { - insert(model.umbracoField); - vm.pageFieldOverlay.show = false; - vm.pageFieldOverlay = null; - }, - close: function (model) { - // close the dialog - vm.pageFieldOverlay.show = false; - vm.pageFieldOverlay = null; - // focus editor - vm.editor.focus(); - } - }; - } - - - function openDictionaryItemOverlay() { - vm.dictionaryItemOverlay = { - view: "treepicker", - section: "settings", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - show: true, - title: "Insert dictionary item", - emptyStateMessage: localizationService.localize("emptyStates_emptyDictionaryTree"), - select: function(node){ - - var code = templateHelper.getInsertDictionarySnippet(node.name); - insert(code); - - vm.dictionaryItemOverlay.show = false; - vm.dictionaryItemOverlay = null; - }, - close: function (model) { - // close dialog - vm.dictionaryItemOverlay.show = false; - vm.dictionaryItemOverlay = null; - // focus editor - vm.editor.focus(); - } - }; - } - - function openQueryBuilderOverlay() { - vm.queryBuilderOverlay = { - view: "querybuilder", - show: true, - title: "Query for content", - - submit: function (model) { - - var code = templateHelper.getQuerySnippet(model.result.queryExpression); - insert(code); - - vm.queryBuilderOverlay.show = false; - vm.queryBuilderOverlay = null; - }, - - close: function (model) { - // close dialog - vm.queryBuilderOverlay.show = false; - vm.queryBuilderOverlay = null; - // focus editor - vm.editor.focus(); - } - }; - } - - /* Local functions */ - - function init() { - //we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css"); - - if ($routeParams.create) { - - var snippet = "Empty"; - - if($routeParams.snippet) { - snippet = $routeParams.snippet; - } - - codefileResource.getScaffold("partialViews", $routeParams.id, snippet).then(function (partialView) { - ready(partialView, false); + } + var search = _.debounce(function () { + $scope.$apply(function () { + getUsers(); }); - - } else { - codefileResource.getByPath('partialViews', $routeParams.id).then(function (partialView) { - ready(partialView, true); - }); - } - - } - - function ready(partialView, syncTree) { - - vm.page.loading = false; - vm.partialView = partialView; - - //sync state - editorState.set(vm.partialView); - - if (syncTree) { - navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; + }, 500); + function searchUsers() { + search(); + } + function getUsers() { + vm.loading = true; + // Get users + usersResource.getPagedResults(vm.usersOptions).then(function (users) { + vm.users = users.items; + vm.usersOptions.pageNumber = users.pageNumber; + vm.usersOptions.pageSize = users.pageSize; + vm.usersOptions.totalItems = users.totalItems; + vm.usersOptions.totalPages = users.totalPages; + preSelect($scope.model.selection, vm.users); + vm.loading = false; }); } - - // ace configuration - vm.aceOption = { - mode: "razor", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px' - }, - onLoad: function(_editor) { - vm.editor = _editor; - - //Update the auto-complete method to use ctrl+alt+space - _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); - - //Unassigns the keybinding (That was previously auto-complete) - //As conflicts with our own tree search shortcut - _editor.commands.bindKey("ctrl-space", null); - - // Assign new keybinding - _editor.commands.addCommands([ - //Disable (alt+shift+K) - //Conflicts with our own show shortcuts dialog - this overrides it - { - name: 'unSelectOrFindPrevious', - bindKey: 'Alt-Shift-K', - exec: function () { - //Toggle the show keyboard shortcuts overlay - $scope.$apply(function () { - vm.showKeyboardShortcut = !vm.showKeyboardShortcut; - }); - }, - readOnly: true - }, - { - name: 'insertUmbracoValue', - bindKey: 'Alt-Shift-V', - exec: function () { - $scope.$apply(function () { - openPageFieldOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertDictionary', - bindKey: 'Alt-Shift-D', - exec: function () { - $scope.$apply(function () { - openDictionaryItemOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertUmbracoMacro', - bindKey: 'Alt-Shift-M', - exec: function () { - $scope.$apply(function () { - openMacroOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertQuery', - bindKey: 'Alt-Shift-Q', - exec: function () { - $scope.$apply(function () { - openQueryBuilderOverlay(); - }); - }, - readOnly: true - }, - - ]); - - // initial cursor placement - // Keep cursor in name field if we are create a new template - // else set the cursor at the bottom of the code editor - if(!$routeParams.create) { - $timeout(function(){ - vm.editor.navigateFileEnd(); - vm.editor.focus(); - persistCurrentLocation(); - }); - } - - //change on blur, focus - vm.editor.on("blur", persistCurrentLocation); - vm.editor.on("focus", persistCurrentLocation); - vm.editor.on("change", changeAceEditor); - - } - } - - } - - function insert(str) { - vm.editor.focus(); - vm.editor.moveCursorToPosition(vm.currentPosition); - vm.editor.insert(str); - - // set form state to $dirty - setFormState("dirty"); - } - - function persistCurrentLocation() { - vm.currentPosition = vm.editor.getCursorPosition(); - } - - function changeAceEditor() { - setFormState("dirty"); - } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if(state === "dirty") { - currentForm.$setDirty(); - } else if(state === "pristine") { - currentForm.$setPristine(); + function changePageNumber(pageNumber) { + vm.usersOptions.pageNumber = pageNumber; + getUsers(); + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Overlays.UserPickerController', UserPickerController); + }()); + angular.module('umbraco').controller('Umbraco.Overlays.YsodController', function ($scope, legacyResource, treeService, navigationService, localizationService) { + if (!$scope.model.title) { + $scope.model.title = localizationService.localize('errors_receivedErrorFromServer'); + } + if ($scope.model.error && $scope.model.error.data && $scope.model.error.data.StackTrace) { + //trim whitespace + $scope.model.error.data.StackTrace = $scope.model.error.data.StackTrace.trim(); + } + if ($scope.model.error && $scope.model.error.data) { + $scope.model.error.data.InnerExceptions = []; + var ex = $scope.model.error.data.InnerException; + while (ex) { + if (ex.StackTrace) { + ex.StackTrace = ex.StackTrace.trim(); + } + $scope.model.error.data.InnerExceptions.push(ex); + ex = ex.InnerException; } } - - - init(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.PartialViews.EditController", PartialViewsEditController); -})(); - -function imageFilePickerController($scope) { - - $scope.pick = function() { - $scope.mediaPickerDialog = {}; - $scope.mediaPickerDialog.view = "mediapicker"; - $scope.mediaPickerDialog.show = true; - - $scope.mediaPickerDialog.submit = function(model) { - $scope.model.value = model.selectedImages[0].image; - $scope.mediaPickerDialog.show = false; - $scope.mediaPickerDialog = null; - }; - - $scope.mediaPickerDialog.close = function(oldModel) { - $scope.mediaPickerDialog.show = false; - $scope.mediaPickerDialog = null; - }; - }; - -} - -angular.module('umbraco').controller("Umbraco.PrevalueEditors.ImageFilePickerController", imageFilePickerController); - -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -function mediaPickerController($scope, dialogService, entityResource, $log, iconHelper) { - - function trim(str, chr) { - var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); - return str.replace(rgxtrim, ''); - } - - $scope.renderModel = []; - - var dialogOptions = { - multiPicker: false, - entityType: "Media", - section: "media", - treeAlias: "media", - idType: "int" - }; - - //combine the dialogOptions with any values returned from the server - if ($scope.model.config) { - angular.extend(dialogOptions, $scope.model.config); - } - - $scope.openContentPicker = function() { - $scope.contentPickerOverlay = dialogOptions; - $scope.contentPickerOverlay.view = "treePicker"; - $scope.contentPickerOverlay.show = true; - - $scope.contentPickerOverlay.submit = function(model) { - - if ($scope.contentPickerOverlay.multiPicker) { - _.each(model.selection, function (item, i) { - $scope.add(item); - }); - } - else { - $scope.clear(); - $scope.add(model.selection[0]); - } - - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - }; - - $scope.contentPickerOverlay.close = function(oldModel) { - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - }; - } - - $scope.remove =function(index, event){ - event.preventDefault(); - $scope.renderModel.splice(index, 1); - }; - - $scope.clear = function() { - $scope.renderModel = []; - }; - - $scope.add = function (item) { - - var itemId = dialogOptions.idType === "udi" ? item.udi : item.id; - - var currIds = _.map($scope.renderModel, function (i) { - return dialogOptions.idType === "udi" ? i.udi : i.id; - }); - if (currIds.indexOf(itemId) < 0) { - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, udi: item.udi }); - } - }; - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var currIds = _.map($scope.renderModel, function (i) { - return dialogOptions.idType === "udi" ? i.udi : i.id; - }); - $scope.model.value = trim(currIds.join(), ","); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); }); - - //load media data - var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; - if (modelIds.length > 0) { - entityResource.getByIds(modelIds, dialogOptions.entityType).then(function (data) { - _.each(data, function (item, i) { - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, udi: item.udi }); - }); + angular.module('umbraco').controller('Umbraco.Editors.Content.CopyController', function ($scope, userService, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { + var dialogOptions = $scope.dialogOptions; + var searchText = 'Search...'; + localizationService.localize('general_search').then(function (value) { + searchText = value + '...'; }); - } - - -} - -angular.module('umbraco').controller("Umbraco.PrevalueEditors.MediaPickerController",mediaPickerController); -angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiValuesController", - function ($scope, $timeout) { - - //NOTE: We need to make each item an object, not just a string because you cannot 2-way bind to a primitive. - - $scope.newItem = ""; - $scope.hasError = false; - - if (!angular.isArray($scope.model.value)) { - - //make an array from the dictionary - var items = []; - for (var i in $scope.model.value) { - items.push({ - value: $scope.model.value[i].value, - sortOrder: $scope.model.value[i].sortOrder, - id: i - }); - } - - //ensure the items are sorted by the provided sort order - items.sort(function (a, b) { return (a.sortOrder > b.sortOrder) ? 1 : ((b.sortOrder > a.sortOrder) ? -1 : 0); }); - - //now make the editor model the array - $scope.model.value = items; - } - - $scope.remove = function(item, evt) { - evt.preventDefault(); - - $scope.model.value = _.reject($scope.model.value, function (x) { - return x.value === item.value; - }); - - }; - - $scope.add = function (evt) { - evt.preventDefault(); - - - if ($scope.newItem) { - if (!_.contains($scope.model.value, $scope.newItem)) { - $scope.model.value.push({ value: $scope.newItem }); - $scope.newItem = ""; - $scope.hasError = false; - return; - } - } - - //there was an error, do the highlight (will be set back by the directive) - $scope.hasError = true; - }; - - $scope.sortableOptions = { - axis: 'y', - containment: 'parent', - cursor: 'move', - items: '> div.control-group', - tolerance: 'pointer', - update: function (e, ui) { - // Get the new and old index for the moved element (using the text as the identifier, so - // we'd have a problem if two prevalues were the same, but that would be unlikely) - var newIndex = ui.item.index(); - var movedPrevalueText = $('input[type="text"]', ui.item).val(); - var originalIndex = getElementIndexByPrevalueText(movedPrevalueText); - - // Move the element in the model - if (originalIndex > -1) { - var movedElement = $scope.model.value[originalIndex]; - $scope.model.value.splice(originalIndex, 1); - $scope.model.value.splice(newIndex, 0, movedElement); - } - } - }; - - function getElementIndexByPrevalueText(value) { - for (var i = 0; i < $scope.model.value.length; i++) { - if ($scope.model.value[i].value === value) { - return i; - } - } - - return -1; - } - - }); - -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -angular.module('umbraco') -.controller("Umbraco.PrevalueEditors.TreePickerController", - - function($scope, dialogService, entityResource, $log, iconHelper){ - $scope.renderModel = []; - $scope.ids = []; - - - var config = { - multiPicker: false, - entityType: "Document", - type: "content", - treeAlias: "content", - idType: "int" - }; - - //combine the config with any values returned from the server - if ($scope.model.config) { - angular.extend(config, $scope.model.config); - } - - if($scope.model.value){ - $scope.ids = $scope.model.value.split(','); - entityResource.getByIds($scope.ids, config.entityType).then(function (data) { - _.each(data, function (item, i) { - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, udi: item.udi }); - }); - }); - } - - $scope.openContentPicker = function() { - $scope.treePickerOverlay = config; - $scope.treePickerOverlay.section = config.type; - $scope.treePickerOverlay.view = "treePicker"; - $scope.treePickerOverlay.show = true; - - $scope.treePickerOverlay.submit = function(model) { - - if(config.multiPicker) { - populate(model.selection); - } else { - populate(model.selection[0]); - } - - $scope.treePickerOverlay.show = false; - $scope.treePickerOverlay = null; - }; - - $scope.treePickerOverlay.close = function(oldModel) { - $scope.treePickerOverlay.show = false; - $scope.treePickerOverlay = null; - }; - - } - - $scope.remove =function(index){ - $scope.renderModel.splice(index, 1); - $scope.ids.splice(index, 1); - $scope.model.value = trim($scope.ids.join(), ","); - }; - - $scope.clear = function() { - $scope.model.value = ""; - $scope.renderModel = []; - $scope.ids = []; - }; - - $scope.add = function (item) { - - var itemId = config.idType === "udi" ? item.udi : item.id; - - if ($scope.ids.indexOf(itemId) < 0){ - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - - $scope.ids.push(itemId); - $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon, udi: item.udi}); - $scope.model.value = trim($scope.ids.join(), ","); - } - }; - - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - $scope.model.value = trim($scope.ids.join(), ","); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - function trim(str, chr) { - var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^'+chr+'+|'+chr+'+$', 'g'); - return str.replace(rgxtrim, ''); - } - - function populate(data){ - if(angular.isArray(data)){ - _.each(data, function (item, i) { - $scope.add(item); - }); - }else{ - $scope.clear(); - $scope.add(data); - } - } -}); - -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -angular.module('umbraco') -.controller("Umbraco.PrevalueEditors.TreeSourceController", - - function($scope, dialogService, entityResource, $log, iconHelper){ - - if (!$scope.model) { - $scope.model = {}; - } - if (!$scope.model.value) { - $scope.model.value = { - type: "content" - }; - } - if (!$scope.model.config) { - $scope.model.config = { - idType: "int" - }; - } - - if($scope.model.value.id && $scope.model.value.type !== "member"){ - var ent = "Document"; - if($scope.model.value.type === "media"){ - ent = "Media"; - } - - entityResource.getById($scope.model.value.id, ent).then(function(item){ - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.node = item; - }); - } - - - $scope.openContentPicker =function(){ - $scope.treePickerOverlay = { - view: "treepicker", - idType: $scope.model.config.idType, - section: $scope.model.value.type, - treeAlias: $scope.model.value.type, - multiPicker: false, - show: true, - submit: function(model) { - var item = model.selection[0]; - populate(item); - $scope.treePickerOverlay.show = false; - $scope.treePickerOverlay = null; - } - }; - }; - - $scope.clear = function() { - $scope.model.value.id = undefined; - $scope.node = undefined; - $scope.model.value.query = undefined; - }; - - - //we always need to ensure we dont submit anything broken - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - if($scope.model.value.type === "member"){ - $scope.model.value.id = -1; - $scope.model.value.query = ""; - } - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - function populate(item){ - $scope.clear(); - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.node = item; - $scope.model.value.id = $scope.model.config.idType === "udi" ? item.udi : item.id; - } -}); -function booleanEditorController($scope, $rootScope, assetsService) { - - function setupViewModel() { - $scope.renderModel = { - value: false - }; - - if ($scope.model.config && $scope.model.config.default && $scope.model.config.default.toString() === "1" && $scope.model && !$scope.model.value) { - $scope.renderModel.value = true; - } - - if ($scope.model && $scope.model.value && ($scope.model.value.toString() === "1" || angular.lowercase($scope.model.value) === "true")) { - $scope.renderModel.value = true; - } - } - - setupViewModel(); - - $scope.$watch("renderModel.value", function (newVal) { - $scope.model.value = newVal === true ? "1" : "0"; - }); - - //here we declare a special method which will be called whenever the value has changed from the server - //this is instead of doing a watch on the model.value = faster - $scope.model.onValueChanged = function (newVal, oldVal) { - //update the display val again if it has changed from the server - setupViewModel(); - }; - -} -angular.module("umbraco").controller("Umbraco.PropertyEditors.BooleanController", booleanEditorController); - -angular.module("umbraco").controller("Umbraco.PropertyEditors.ChangePasswordController", - function ($scope, $routeParams) { - - function resetModel(isNew) { - //the model config will contain an object, if it does not we'll create defaults - //NOTE: We will not support doing the password regex on the client side because the regex on the server side - //based on the membership provider cannot always be ported to js from .net directly. - /* - { - hasPassword: true/false, - requiresQuestionAnswer: true/false, - enableReset: true/false, - enablePasswordRetrieval: true/false, - minPasswordLength: 10 - } - */ - - //set defaults if they are not available - if (!$scope.model.config || $scope.model.config.disableToggle === undefined) { - $scope.model.config.disableToggle = false; - } - if (!$scope.model.config || $scope.model.config.hasPassword === undefined) { - $scope.model.config.hasPassword = false; - } - if (!$scope.model.config || $scope.model.config.enablePasswordRetrieval === undefined) { - $scope.model.config.enablePasswordRetrieval = true; - } - if (!$scope.model.config || $scope.model.config.requiresQuestionAnswer === undefined) { - $scope.model.config.requiresQuestionAnswer = false; - } - if (!$scope.model.config || $scope.model.config.enableReset === undefined) { - $scope.model.config.enableReset = true; - } - if (!$scope.model.config || $scope.model.config.minPasswordLength === undefined) { - $scope.model.config.minPasswordLength = 0; - } - - //set the model defaults - if (!angular.isObject($scope.model.value)) { - //if it's not an object then just create a new one - $scope.model.value = { - newPassword: null, - oldPassword: null, - reset: null, - answer: null - }; - } - else { - //just reset the values - - if (!isNew) { - //if it is new, then leave the generated pass displayed - $scope.model.value.newPassword = null; - $scope.model.value.oldPassword = null; - } - $scope.model.value.reset = null; - $scope.model.value.answer = null; - } - - //the value to compare to match passwords - if (!isNew) { - $scope.model.confirm = ""; - } - else if ($scope.model.value.newPassword && $scope.model.value.newPassword.length > 0) { - //if it is new and a new password has been set, then set the confirm password too - $scope.model.confirm = $scope.model.value.newPassword; - } - - } - - resetModel($routeParams.create); - - //if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there - //with validators turned on. - $scope.changing = $scope.model.config.disableToggle === true || !$scope.model.config.hasPassword; - - //we're not currently changing so set the model to null - if (!$scope.changing) { - $scope.model.value = null; - } - - $scope.doChange = function() { - resetModel(); - $scope.changing = true; - //if there was a previously generated password displaying, clear it - $scope.model.value.generatedPassword = null; - }; - - $scope.cancelChange = function() { - $scope.changing = false; - //set model to null - $scope.model.value = null; - }; - - var unsubscribe = []; - - //listen for the saved event, when that occurs we'll - //change to changing = false; - unsubscribe.push($scope.$on("formSubmitted", function() { - if ($scope.model.config.disableToggle === false) { - $scope.changing = false; - } - })); - unsubscribe.push($scope.$on("formSubmitting", function() { - //if there was a previously generated password displaying, clear it - if ($scope.changing && $scope.model.value) { - $scope.model.value.generatedPassword = null; - } - else if (!$scope.changing) { - //we are not changing, so the model needs to be null - $scope.model.value = null; - } - })); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - - $scope.showReset = function() { - return $scope.model.config.hasPassword && $scope.model.config.enableReset; - }; - - $scope.showOldPass = function() { - return $scope.model.config.hasPassword && - !$scope.model.config.allowManuallyChangingPassword && - !$scope.model.config.enablePasswordRetrieval && !$scope.model.value.reset; - }; - - $scope.showNewPass = function () { - return !$scope.model.value.reset; - }; - - $scope.showConfirmPass = function() { - return !$scope.model.value.reset; - }; - - $scope.showCancelBtn = function() { - return $scope.model.config.disableToggle !== true && $scope.model.config.hasPassword; - }; - - }); - -angular.module("umbraco").controller("Umbraco.PropertyEditors.CheckboxListController", - function($scope) { - - if (angular.isObject($scope.model.config.items)) { - - //now we need to format the items in the dictionary because we always want to have an array - var newItems = []; - var vals = _.values($scope.model.config.items); - var keys = _.keys($scope.model.config.items); - for (var i = 0; i < vals.length; i++) { - newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: vals[i].value }); - } - - //ensure the items are sorted by the provided sort order - newItems.sort(function (a, b) { return (a.sortOrder > b.sortOrder) ? 1 : ((b.sortOrder > a.sortOrder) ? -1 : 0); }); - - //re-assign - $scope.model.config.items = newItems; - - } - - function setupViewModel() { - $scope.selectedItems = []; - - //now we need to check if the value is null/undefined, if it is we need to set it to "" so that any value that is set - // to "" gets selected by default - if ($scope.model.value === null || $scope.model.value === undefined) { - $scope.model.value = []; - } - - for (var i = 0; i < $scope.model.config.items.length; i++) { - var isChecked = _.contains($scope.model.value, $scope.model.config.items[i].id); - $scope.selectedItems.push({ - checked: isChecked, - key: $scope.model.config.items[i].id, - val: $scope.model.config.items[i].value - }); - } - - } - - setupViewModel(); - - - //update the model when the items checked changes - $scope.$watch("selectedItems", function(newVal, oldVal) { - - $scope.model.value = []; - for (var x = 0; x < $scope.selectedItems.length; x++) { - if ($scope.selectedItems[x].checked) { - $scope.model.value.push($scope.selectedItems[x].key); - } - } - - }, true); - - //here we declare a special method which will be called whenever the value has changed from the server - //this is instead of doing a watch on the model.value = faster - $scope.model.onValueChanged = function (newVal, oldVal) { - //update the display val again if it has changed from the server - setupViewModel(); - }; - - }); - -function ColorPickerController($scope) { - $scope.toggleItem = function (color) { - if ($scope.model.value == color) { - $scope.model.value = ""; - //this is required to re-validate - $scope.propertyForm.modelValue.$setViewValue($scope.model.value); - } - else { - $scope.model.value = color; - //this is required to re-validate - $scope.propertyForm.modelValue.$setViewValue($scope.model.value); - } - }; - // Method required by the valPropertyValidator directive (returns true if the property editor has at least one color selected) - $scope.validateMandatory = function () { - return { - isValid: !$scope.model.validation.mandatory || ($scope.model.value != null && $scope.model.value != ""), - errorMsg: "Value cannot be empty", - errorKey: "required" - }; - } - $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; -} - -angular.module("umbraco").controller("Umbraco.PropertyEditors.ColorPickerController", ColorPickerController); - -angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiColorPickerController", - function ($scope, $timeout, assetsService, angularHelper, $element) { - //NOTE: We need to make each color an object, not just a string because you cannot 2-way bind to a primitive. - var defaultColor = "000000"; - - $scope.newColor = defaultColor; - $scope.hasError = false; - - assetsService.load([ - //"lib/spectrum/tinycolor.js", - "lib/spectrum/spectrum.js" - ], $scope).then(function () { - var elem = $element.find("input"); - elem.spectrum({ - color: null, - showInitial: false, - chooseText: "choose", // TODO: These can be localised - cancelText: "cancel", // TODO: These can be localised - preferredFormat: "hex", - showInput: true, - clickoutFiresChange: true, - hide: function (color) { - //show the add butotn - $element.find(".btn.add").show(); - }, - change: function (color) { - angularHelper.safeApply($scope, function () { - $scope.newColor = color.toHexString().trimStart("#"); // #ff0000 - }); - }, - show: function() { - //hide the add butotn - $element.find(".btn.add").hide(); - } - }); + $scope.relateToOriginal = true; + $scope.recursive = true; + $scope.dialogTreeEventHandler = $({}); + $scope.busy = false; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + $scope.treeModel = { hideHeader: false }; + userService.getCurrentUser().then(function (userData) { + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); - - if (!angular.isArray($scope.model.value)) { - //make an array from the dictionary - var items = []; - for (var i in $scope.model.value) { - items.push({ - value: $scope.model.value[i], - id: i - }); + var node = dialogOptions.currentNode; + function nodeSelectHandler(ev, args) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); } - //now make the editor model the array - $scope.model.value = items; + eventsService.emit('editors.content.copyController.select', args); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; } - - $scope.remove = function (item, evt) { - - evt.preventDefault(); - - $scope.model.value = _.reject($scope.model.value, function (x) { - return x.value === item.value; + function nodeExpandedHandler(ev, args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + }; + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { + event: evt, + node: result }); - }; - - $scope.add = function (evt) { - - evt.preventDefault(); - - if ($scope.newColor) { - var exists = _.find($scope.model.value, function(item) { - return item.value.toUpperCase() == $scope.newColor.toUpperCase(); + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + $scope.copy = function () { + $scope.busy = true; + $scope.error = false; + contentResource.copy({ + parentId: $scope.target.id, + id: node.id, + relateToOriginal: $scope.relateToOriginal, + recursive: $scope.recursive + }).then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + navigationService.syncTree({ + tree: 'content', + path: path, + forceReload: true, + activate: false + }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ + tree: 'content', + path: activeNodePath, + forceReload: false, + activate: true + }); + } }); - if (!exists) { - $scope.model.value.push({ value: $scope.newColor }); - //$scope.newColor = defaultColor; - // set colorpicker to default color - //var elem = $element.find("input"); - //elem.spectrum("set", $scope.newColor); - $scope.hasError = false; - return; + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } } - - //there was an error, do the highlight (will be set back by the directive) - $scope.hasError = true; - } - + }); }; - - //load the separate css for the editor to avoid it blocking our js loading - assetsService.loadCss("lib/spectrum/spectrum.css"); + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); + }); + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + function openMiniListView(node) { + $scope.miniListView = node; + } }); - -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it - -function contentPickerController($scope, entityResource, editorState, iconHelper, $routeParams, angularHelper, navigationService, $location, miniEditorHelper) { - - function trim(str, chr) { - var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); - return str.replace(rgxtrim, ''); - } - - function startWatch() { - //We need to watch our renderModel so that we can update the underlying $scope.model.value properly, this is required - // because the ui-sortable doesn't dispatch an event after the digest of the sort operation. Any of the events for UI sortable - // occur after the DOM has updated but BEFORE the digest has occured so the model has NOT changed yet - it even states so in the docs. - // In their source code there is no event so we need to just subscribe to our model changes here. - //This also makes it easier to manage models, we update one and the rest will just work. - $scope.$watch(function () { - //return the joined Ids as a string to watch - return _.map($scope.renderModel, function (i) { - return $scope.model.config.idType === "udi" ? i.udi : i.id; - }).join(); - }, function (newVal) { - var currIds = _.map($scope.renderModel, function (i) { - return $scope.model.config.idType === "udi" ? i.udi : i.id; - }); - $scope.model.value = trim(currIds.join(), ","); - - //Validate! - if ($scope.model.config && $scope.model.config.minNumber && parseInt($scope.model.config.minNumber) > $scope.renderModel.length) { - $scope.contentPickerForm.minCount.$setValidity("minCount", false); - } - else { - $scope.contentPickerForm.minCount.$setValidity("minCount", true); - } - - if ($scope.model.config && $scope.model.config.maxNumber && parseInt($scope.model.config.maxNumber) < $scope.renderModel.length) { - $scope.contentPickerForm.maxCount.$setValidity("maxCount", false); - } - else { - $scope.contentPickerForm.maxCount.$setValidity("maxCount", true); - } - - setSortingState($scope.renderModel); - - }); - } - - $scope.renderModel = []; - - $scope.dialogEditor = editorState && editorState.current && editorState.current.isDialogEditor === true; - - //the default pre-values - var defaultConfig = { - multiPicker: false, - showOpenButton: false, - showEditButton: false, - showPathOnHover: false, - startNode: { - query: "", - type: "content", - id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker - } - }; - - // sortable options - $scope.sortableOptions = { - distance: 10, - tolerance: "pointer", - scroll: true, - zIndex: 6000 - }; - - if ($scope.model.config) { - //merge the server config on top of the default config, then set the server config to use the result - $scope.model.config = angular.extend(defaultConfig, $scope.model.config); - } - - //Umbraco persists boolean for prevalues as "0" or "1" so we need to convert that! - $scope.model.config.multiPicker = ($scope.model.config.multiPicker === "1" ? true : false); - $scope.model.config.showOpenButton = ($scope.model.config.showOpenButton === "1" ? true : false); - $scope.model.config.showEditButton = ($scope.model.config.showEditButton === "1" ? true : false); - $scope.model.config.showPathOnHover = ($scope.model.config.showPathOnHover === "1" ? true : false); - - var entityType = $scope.model.config.startNode.type === "member" - ? "Member" - : $scope.model.config.startNode.type === "media" - ? "Media" - : "Document"; - $scope.allowOpenButton = entityType === "Document"; - $scope.allowEditButton = entityType === "Document"; - $scope.allowRemoveButton = true; - - //the dialog options for the picker - var dialogOptions = { - multiPicker: $scope.model.config.multiPicker, - entityType: entityType, - filterCssClass: "not-allowed not-published", - startNodeId: null, - callback: function (data) { - if (angular.isArray(data)) { - _.each(data, function (item, i) { - $scope.add(item); - }); - } else { - $scope.clear(); - $scope.add(data); - } - angularHelper.getCurrentForm($scope).$setDirty(); - }, - treeAlias: $scope.model.config.startNode.type, - section: $scope.model.config.startNode.type, - idType: "int" - }; - - //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the - // pre-value config on to the dialog options - angular.extend(dialogOptions, $scope.model.config); - - //We need to manually handle the filter for members here since the tree displayed is different and only contains - // searchable list views - if (entityType === "Member") { - //first change the not allowed filter css class - dialogOptions.filterCssClass = "not-allowed"; - var currFilter = dialogOptions.filter; - //now change the filter to be a method - dialogOptions.filter = function(i) { - //filter out the list view nodes - if (i.metaData.isContainer) { - return true; - } - if (!currFilter) { - return false; - } - //now we need to filter based on what is stored in the pre-vals, this logic duplicates what is in the treepicker.controller, - // but not much we can do about that since members require special filtering. - var filterItem = currFilter.toLowerCase().split(','); - var found = filterItem.indexOf(i.metaData.contentType.toLowerCase()) >= 0; - if (!currFilter.startsWith("!") && !found || currFilter.startsWith("!") && found) { - return true; - } - - return false; - } - } - - - //if we have a query for the startnode, we will use that. - if ($scope.model.config.startNode.query) { - var rootId = $routeParams.id; - entityResource.getByQuery($scope.model.config.startNode.query, rootId, "Document").then(function (ent) { - dialogOptions.startNodeId = $scope.model.config.idType === "udi" ? ent.udi : ent.id; - }); - } - else { - dialogOptions.startNodeId = $scope.model.config.startNode.id; - } - - //dialog - $scope.openContentPicker = function() { - $scope.contentPickerOverlay = dialogOptions; - $scope.contentPickerOverlay.view = "treepicker"; - $scope.contentPickerOverlay.show = true; - - $scope.contentPickerOverlay.submit = function(model) { - - if (angular.isArray(model.selection)) { - _.each(model.selection, function (item, i) { - $scope.add(item); - }); - } - - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - } - - $scope.contentPickerOverlay.close = function(oldModel) { - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - } - - }; - - $scope.remove = function (index) { - $scope.renderModel.splice(index, 1); - angularHelper.getCurrentForm($scope).$setDirty(); - }; - - $scope.showNode = function (index) { - var item = $scope.renderModel[index]; - var id = item.id; - var section = $scope.model.config.startNode.type.toLowerCase(); - - entityResource.getPath(id, entityType).then(function (path) { - navigationService.changeSection(section); - navigationService.showTree(section, { - tree: section, path: path, forceReload: false, activate: true - }); - var routePath = section + "/" + section + "/edit/" + id.toString(); - $location.path(routePath).search(""); - }); - } - - $scope.add = function (item) { - var currIds = _.map($scope.renderModel, function (i) { - return $scope.model.config.idType === "udi" ? i.udi : i.id; - }); - - var itemId = $scope.model.config.idType === "udi" ? item.udi : item.id; - - if (currIds.indexOf(itemId) < 0) { - setEntityUrl(item); - } - }; - - $scope.clear = function () { - $scope.renderModel = []; - }; - - $scope.openMiniEditor = function(node) { - miniEditorHelper.launchMiniEditor(node).then(function(updatedNode){ - // update the node - node.name = updatedNode.name; - node.published = updatedNode.hasPublishedVersion; - if(entityType !== "Member") { - entityResource.getUrl(updatedNode.id, entityType).then(function(data){ - node.url = data; - }); - } - }); - }; - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var currIds = _.map($scope.renderModel, function (i) { - return $scope.model.config.idType === "udi" ? i.udi : i.id; - }); - $scope.model.value = trim(currIds.join(), ","); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - - var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; - - //load current data if anything selected - if (modelIds.length > 0) { - entityResource.getByIds(modelIds, entityType).then(function(data) { - - _.each(modelIds, - function(id, i) { - var entity = _.find(data, - function(d) { - return $scope.model.config.idType === "udi" ? (d.udi == id) : (d.id == id); - }); - - if (entity) { - setEntityUrl(entity); - } - - }); - - //everything is loaded, start the watch on the model - startWatch(); - - }); - } - else { - //everything is loaded, start the watch on the model - startWatch(); - } - - - function setEntityUrl(entity) { - - // get url for content and media items - if(entityType !== "Member") { - entityResource.getUrl(entity.id, entityType).then(function(data){ - // update url - angular.forEach($scope.renderModel, function(item){ - if(item.id === entity.id) { - item.url = data; - } - }); - }); - } - - // add the selected item to the renderModel - // if it needs to show a url the item will get - // updated when the url comes back from server - addSelectedItem(entity); - - } - - function addSelectedItem(item) { - - // set icon - if(item.icon) { - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - } - - // set default icon - if (!item.icon) { - switch (entityType) { - case "Document": - item.icon = "icon-document"; - break; - case "Media": - item.icon = "icon-picture"; - break; - case "Member": - item.icon = "icon-user"; - break; - } - } - - $scope.renderModel.push({ - "name": item.name, - "id": item.id, - "udi": item.udi, - "icon": item.icon, - "path": item.path, - "url": item.url, - "published": (item.metaData && item.metaData.IsPublished === false && entityType === "Document") ? false : true - // only content supports published/unpublished content so we set everything else to published so the UI looks correct - }); - - } - - function setSortingState(items) { - // disable sorting if the list only consist of one item - if(items.length > 1) { - $scope.sortableOptions.disabled = false; - } else { - $scope.sortableOptions.disabled = true; - } - } - -} - -angular.module('umbraco').controller("Umbraco.PropertyEditors.ContentPickerController", contentPickerController); - -function dateTimePickerController($scope, notificationsService, assetsService, angularHelper, userService, $element, dateHelper) { - - //setup the default config - var config = { - pickDate: true, - pickTime: true, - useSeconds: true, - format: "YYYY-MM-DD HH:mm:ss", - icons: { - time: "icon-time", - date: "icon-calendar", - up: "icon-chevron-up", - down: "icon-chevron-down" - } - - }; - - //map the user config - $scope.model.config = angular.extend(config, $scope.model.config); - //ensure the format doesn't get overwritten with an empty string - if ($scope.model.config.format === "" || $scope.model.config.format === undefined || $scope.model.config.format === null) { - $scope.model.config.format = $scope.model.config.pickTime ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD"; - } - - - - $scope.hasDatetimePickerValue = $scope.model.value ? true : false; - $scope.datetimePickerValue = null; - - //hide picker if clicking on the document - $scope.hidePicker = function () { - //$element.find("div:first").datetimepicker("hide"); - // Sometimes the statement above fails and generates errors in the browser console. The following statements fix that. - var dtp = $element.find("div:first"); - if (dtp && dtp.datetimepicker) { - dtp.datetimepicker("hide"); - } - }; - $(document).bind("click", $scope.hidePicker); - - //handles the date changing via the api - function applyDate(e) { - angularHelper.safeApply($scope, function() { - // when a date is changed, update the model - if (e.date && e.date.isValid()) { - $scope.datePickerForm.datepicker.$setValidity("pickerError", true); - $scope.hasDatetimePickerValue = true; - $scope.datetimePickerValue = e.date.format($scope.model.config.format); - } - else { - $scope.hasDatetimePickerValue = false; - $scope.datetimePickerValue = null; - } - - setModelValue(); - - if (!$scope.model.config.pickTime) { - $element.find("div:first").datetimepicker("hide", 0); - } - }); - } - - //sets the scope model value accordingly - this is the value to be sent up to the server and depends on - // if the picker is configured to offset time. We always format the date/time in a specific format for sending - // to the server, this is different from the format used to display the date/time. - function setModelValue() { - if ($scope.hasDatetimePickerValue) { - var elementData = $element.find("div:first").data().DateTimePicker; - if ($scope.model.config.pickTime) { - //check if we are supposed to offset the time - if ($scope.model.value && $scope.model.config.offsetTime === "1" && Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { - $scope.model.value = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset); - $scope.serverTime = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset, "YYYY-MM-DD HH:mm:ss Z"); - } - else { - $scope.model.value = elementData.getDate().format("YYYY-MM-DD HH:mm:ss"); - } - } - else { - $scope.model.value = elementData.getDate().format("YYYY-MM-DD"); - } - } - else { - $scope.model.value = null; - } - } - - var picker = null; - - $scope.clearDate = function() { - $scope.hasDatetimePickerValue = false; - $scope.datetimePickerValue = null; - $scope.model.value = null; - $scope.datePickerForm.datepicker.$setValidity("pickerError", true); - } - - $scope.serverTime = null; - $scope.serverTimeNeedsOffsetting = false; - if (Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { - // Will return something like 120 - var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; - - // Will return something like -120 - var localOffset = new Date().getTimezoneOffset(); - - // If these aren't equal then offsetting is needed - // note the minus in front of serverOffset needed - // because C# and javascript return the inverse offset - $scope.serverTimeNeedsOffsetting = (-serverOffset !== localOffset); - } - - //get the current user to see if we can localize this picker - userService.getCurrentUser().then(function (user) { - - assetsService.loadCss('lib/datetimepicker/bootstrap-datetimepicker.min.css').then(function() { - - var filesToLoad = ["lib/moment/moment-with-locales.js", - "lib/datetimepicker/bootstrap-datetimepicker.js"]; - - - $scope.model.config.language = user.locale; - - - assetsService.load(filesToLoad, $scope).then( - function () { - //The Datepicker js and css files are available and all components are ready to use. - - // Get the id of the datepicker button that was clicked - var pickerId = $scope.model.alias; - - var element = $element.find("div:first"); - - // Open the datepicker and add a changeDate eventlistener - element - .datetimepicker(angular.extend({ useCurrent: true }, $scope.model.config)) - .on("dp.change", applyDate) - .on("dp.error", function(a, b, c) { - $scope.hasDatetimePickerValue = false; - $scope.datePickerForm.datepicker.$setValidity("pickerError", false); - }); - - if ($scope.hasDatetimePickerValue) { - var dateVal; - //check if we are supposed to offset the time - if ($scope.model.value && $scope.model.config.offsetTime === "1" && $scope.serverTimeNeedsOffsetting) { - //get the local time offset from the server - dateVal = dateHelper.convertToLocalMomentTime($scope.model.value, Umbraco.Sys.ServerVariables.application.serverTimeOffset); - $scope.serverTime = dateHelper.convertToServerStringTime(dateVal, Umbraco.Sys.ServerVariables.application.serverTimeOffset, "YYYY-MM-DD HH:mm:ss Z"); - } - else { - //create a normal moment , no offset required - var dateVal = $scope.model.value ? moment($scope.model.value, "YYYY-MM-DD HH:mm:ss") : moment(); - } - - element.datetimepicker("setValue", dateVal); - $scope.datetimePickerValue = dateVal.format($scope.model.config.format); - } - - element.find("input").bind("blur", function() { - //we need to force an apply here - $scope.$apply(); - }); - - //Ensure to remove the event handler when this instance is destroyted - $scope.$on('$destroy', function () { - element.find("input").unbind("blur"); - element.datetimepicker("destroy"); - }); - - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - setModelValue(); - }); - //unbind doc click event! - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - - }); - }); - - }); - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - setModelValue(); - }); - - //unbind doc click event! - $scope.$on('$destroy', function () { - $(document).unbind("click", $scope.hidePicker); - unsubscribe(); - }); -} - -angular.module("umbraco").controller("Umbraco.PropertyEditors.DatepickerController", dateTimePickerController); - -angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownController", - function($scope) { - - //setup the default config - var config = { - items: [], - multiple: false - }; - - //map the user config - angular.extend(config, $scope.model.config); - - //map back to the model - $scope.model.config = config; - - function convertArrayToDictionaryArray(model){ - //now we need to format the items in the dictionary because we always want to have an array - var newItems = []; - for (var i = 0; i < model.length; i++) { - newItems.push({ id: model[i], sortOrder: 0, value: model[i] }); - } - - return newItems; - } - - - function convertObjectToDictionaryArray(model){ - //now we need to format the items in the dictionary because we always want to have an array - var newItems = []; - var vals = _.values($scope.model.config.items); - var keys = _.keys($scope.model.config.items); - - for (var i = 0; i < vals.length; i++) { - var label = vals[i].value ? vals[i].value : vals[i]; - newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: label }); - } - - return newItems; - } - - if (angular.isArray($scope.model.config.items)) { - //PP: I dont think this will happen, but we have tests that expect it to happen.. - //if array is simple values, convert to array of objects - if(!angular.isObject($scope.model.config.items[0])){ - $scope.model.config.items = convertArrayToDictionaryArray($scope.model.config.items); - } - } - else if (angular.isObject($scope.model.config.items)) { - $scope.model.config.items = convertObjectToDictionaryArray($scope.model.config.items); - } - else { - throw "The items property must be either an array or a dictionary"; - } - - - //sort the values - $scope.model.config.items.sort(function (a, b) { return (a.sortOrder > b.sortOrder) ? 1 : ((b.sortOrder > a.sortOrder) ? -1 : 0); }); - - //now we need to check if the value is null/undefined, if it is we need to set it to "" so that any value that is set - // to "" gets selected by default - if ($scope.model.value === null || $scope.model.value === undefined) { - if ($scope.model.config.multiple) { - $scope.model.value = []; - } - else { - $scope.model.value = ""; - } - } - - }); - -/** A drop down list or multi value select list based on an entity type, this can be re-used for any entity types */ -function entityPicker($scope, entityResource) { - - //set the default to DocumentType - if (!$scope.model.config.entityType) { - $scope.model.config.entityType = "DocumentType"; - } - - //Determine the select list options and which value to publish - if (!$scope.model.config.publishBy) { - $scope.selectOptions = "entity.id as entity.name for entity in entities"; - } - else { - $scope.selectOptions = "entity." + $scope.model.config.publishBy + " as entity.name for entity in entities"; - } - - entityResource.getAll($scope.model.config.entityType).then(function (data) { - //convert the ids to strings so the drop downs work properly when comparing - _.each(data, function(d) { - d.id = d.id.toString(); - }); - $scope.entities = data; - }); - - if ($scope.model.value === null || $scope.model.value === undefined) { - if ($scope.model.config.multiple) { - $scope.model.value = []; - } - else { - $scope.model.value = ""; - } - } - else { - //if it's multiple, change the value to an array - if ($scope.model.config.multiple === "1") { - if (_.isString($scope.model.value)) { - $scope.model.value = $scope.model.value.split(','); - } - } - } -} -angular.module('umbraco').controller("Umbraco.PropertyEditors.EntityPickerController", entityPicker); -/** + /** * @ngdoc controller - * @name Umbraco.Editors.FileUploadController + * @name Umbraco.Editors.Content.CreateController * @function - * + * * @description - * The controller for the file upload property editor. It is important to note that the $scope.model.value - * doesn't necessarily depict what is saved for this property editor. $scope.model.value can be empty when we - * are submitting files because in that case, we are adding files to the fileManager which is what gets peristed - * on the server. However, when we are clearing files, we are setting $scope.model.value to "{clearFiles: true}" - * to indicate on the server that we are removing files for this property. We will keep the $scope.model.value to - * be the name of the file selected (if it is a newly selected file) or keep it to be it's original value, this allows - * for the editors to check if the value has changed and to re-bind the property if that is true. - * -*/ -function fileUploadController($scope, $element, $compile, imageHelper, fileManager, umbRequestHelper, mediaHelper) { - - /** Clears the file collections when content is saving (if we need to clear) or after saved */ - function clearFiles() { - //clear the files collection (we don't want to upload any!) - fileManager.setFiles($scope.model.alias, []); - //clear the current files - $scope.files = []; - - if($scope.propertyForm) { - if ($scope.propertyForm.fileCount) { - //this is required to re-validate - $scope.propertyForm.fileCount.$setViewValue($scope.files.length); - } - } - - } - - /** this method is used to initialize the data and to re-initialize it if the server value is changed */ - function initialize(index) { - - clearFiles(); - - if (!index) { - index = 1; - } - - //this is used in order to tell the umb-single-file-upload directive to - //rebuild the html input control (and thus clearing the selected file) since - //that is the only way to manipulate the html for the file input control. - $scope.rebuildInput = { - index: index - }; - //clear the current files - $scope.files = []; - //store the original value so we can restore it if the user clears and then cancels clearing. - $scope.originalValue = $scope.model.value; - - //create the property to show the list of files currently saved - if ($scope.model.value != "" && $scope.model.value != undefined) { - - var images = $scope.model.value.split(","); - - $scope.persistedFiles = _.map(images, function (item) { - return { file: item, isImage: imageHelper.detectIfImageByExtension(item) }; - }); - } - else { - $scope.persistedFiles = []; - } - - _.each($scope.persistedFiles, function (file) { - - var thumbnailUrl = umbRequestHelper.getApiUrl( - "imagesApiBaseUrl", - "GetBigThumbnail", - [{ originalImagePath: file.file }]); - - var extension = file.file.substring(file.file.lastIndexOf(".") + 1, file.file.length); - - file.thumbnail = thumbnailUrl; - file.extension = extension.toLowerCase(); - }); - - $scope.clearFiles = false; - } - - initialize(); - - // Method required by the valPropertyValidator directive (returns true if the property editor has at least one file selected) - $scope.validateMandatory = function () { - return { - isValid: !$scope.model.validation.mandatory || ((($scope.persistedFiles != null && $scope.persistedFiles.length > 0) || ($scope.files != null && $scope.files.length > 0)) && !$scope.clearFiles), - errorMsg: "Value cannot be empty", - errorKey: "required" - }; - } - - //listen for clear files changes to set our model to be sent up to the server - $scope.$watch("clearFiles", function (isCleared) { - if (isCleared == true) { - $scope.model.value = { clearFiles: true }; - clearFiles(); - } - else { - //reset to original value - $scope.model.value = $scope.originalValue; - //this is required to re-validate - if($scope.propertyForm) { - $scope.propertyForm.fileCount.$setViewValue($scope.files.length); - } - } - }); - - //listen for when a file is selected - $scope.$on("filesSelected", function (event, args) { - $scope.$apply(function () { - //set the files collection - fileManager.setFiles($scope.model.alias, args.files); - //clear the current files - $scope.files = []; - var newVal = ""; - for (var i = 0; i < args.files.length; i++) { - //save the file object to the scope's files collection - $scope.files.push({ alias: $scope.model.alias, file: args.files[i] }); - newVal += args.files[i].name + ","; - } - - //this is required to re-validate - $scope.propertyForm.fileCount.$setViewValue($scope.files.length); - - //set clear files to false, this will reset the model too - $scope.clearFiles = false; - //set the model value to be the concatenation of files selected. Please see the notes - // in the description of this controller, it states that this value isn't actually used for persistence, - // but we need to set it so that the editor and the server can detect that it's been changed, and it is used for validation. - $scope.model.value = { selectedFiles: newVal.trimEnd(",") }; - }); - }); - - //listen for when the model value has changed - $scope.$watch("model.value", function (newVal, oldVal) { - //cannot just check for !newVal because it might be an empty string which we - //want to look for. - if (newVal !== null && newVal !== undefined && newVal !== oldVal) { - // here we need to check if the value change needs to trigger an update in the UI. - // if the value is only changed in the controller and not in the server values, we do not - // want to trigger an update yet. - // we can however no longer rely on checking values in the controller vs. values from the server - // to determine whether to update or not, since you could potentially be uploading a file with - // the exact same name - in that case we need to reinitialize to show the newly uploaded file. - if (newVal.clearFiles !== true && !newVal.selectedFiles) { - initialize($scope.rebuildInput.index + 1); - } - } - }); -}; -angular.module("umbraco") - .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController) - .run(function(mediaHelper, umbRequestHelper, assetsService){ - if (mediaHelper && mediaHelper.registerFileResolver) { - assetsService.load(["lib/moment/moment-with-locales.js"]).then( - function () { - //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource - // they contain different data structures so if we need to query against it we need to be aware of this. - mediaHelper.registerFileResolver("Umbraco.UploadField", function(property, entity, thumbnail){ - if (thumbnail) { - if (mediaHelper.detectIfImageByExtension(property.value)) { - //get default big thumbnail from image processor - var thumbnailUrl = property.value + "?rnd=" + moment(entity.updateDate).format("YYYYMMDDHHmmss") + "&width=500&animationprocessmode=first"; - return thumbnailUrl; - } - else { - return null; - } - } - else { - return property.value; - } - }); - } - ); - } - }); - -angular.module("umbraco") - -//this controller is obsolete and should not be used anymore -//it proxies everything to the system media list view which has overtaken -//all the work this property editor used to perform -.controller("Umbraco.PropertyEditors.FolderBrowserController", - function ($rootScope, $scope, contentTypeResource) { - //get the system media listview - contentTypeResource.getPropertyTypeScaffold(-96) - .then(function(dt) { - - $scope.fakeProperty = { - alias: "contents", - config: dt.config, - description: "", - editor: dt.editor, - hideLabel: true, - id: 1, - label: "Contents:", - validation: { - mandatory: false, - pattern: null - }, - value: "", - view: dt.view - }; - - }); -}); - -angular.module("umbraco") -.controller("Umbraco.PropertyEditors.GoogleMapsController", - function ($element, $rootScope, $scope, notificationsService, dialogService, assetsService, $log, $timeout) { - - assetsService.loadJs('https://www.google.com/jsapi') - .then(function () { - google.load("maps", "3", - { - callback: initMap, - other_params: "sensor=false" - }); - }); - - function initMap() { - //Google maps is available and all components are ready to use. - var valueArray = $scope.model.value.split(','); - var latLng = new google.maps.LatLng(valueArray[0], valueArray[1]); - var mapDiv = document.getElementById($scope.model.alias + '_map'); - var mapOptions = { - zoom: $scope.model.config.zoom, - center: latLng, - mapTypeId: google.maps.MapTypeId[$scope.model.config.mapType] - }; - var geocoder = new google.maps.Geocoder(); - var map = new google.maps.Map(mapDiv, mapOptions); - - var marker = new google.maps.Marker({ - map: map, - position: latLng, - draggable: true - }); - - google.maps.event.addListener(map, 'click', function (event) { - - dialogService.mediaPicker({ - callback: function (data) { - var image = data.selection[0].src; - - var latLng = event.latLng; - var marker = new google.maps.Marker({ - map: map, - icon: image, - position: latLng, - draggable: true - }); - - google.maps.event.addListener(marker, "dragend", function (e) { - var newLat = marker.getPosition().lat(); - var newLng = marker.getPosition().lng(); - - codeLatLng(marker.getPosition(), geocoder); - - //set the model value - $scope.model.vvalue = newLat + "," + newLng; - }); - - } - }); - }); - - var tabShown = function(e) { - google.maps.event.trigger(map, 'resize'); - }; - - //listen for tab changes - if (tabsCtrl != null) { - tabsCtrl.onTabShown(function (args) { - tabShown(); - }); - } - - $element.closest('.umb-panel.tabbable').on('shown', '.nav-tabs a', tabShown); - - $scope.$on('$destroy', function () { - $element.closest('.umb-panel.tabbable').off('shown', '.nav-tabs a', tabShown); - }); - } - - function codeLatLng(latLng, geocoder) { - geocoder.geocode({ 'latLng': latLng }, - function (results, status) { - if (status == google.maps.GeocoderStatus.OK) { - var location = results[0].formatted_address; - $rootScope.$apply(function () { - notificationsService.success("Peter just went to: ", location); - }); - } - }); - } - - //here we declare a special method which will be called whenever the value has changed from the server - //this is instead of doing a watch on the model.value = faster - $scope.model.onValueChanged = function (newVal, oldVal) { - //update the display val again if it has changed from the server - initMap(); - }; - }); -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.GridPrevalueEditor.LayoutConfigController", - function ($scope) { - - $scope.currentLayout = $scope.model.currentLayout; - $scope.columns = $scope.model.columns; - $scope.rows = $scope.model.rows; - - $scope.scaleUp = function(section, max, overflow){ - var add = 1; - if(overflow !== true){ - add = (max > 1) ? 1 : max; - } - //var add = (max > 1) ? 1 : max; - section.grid = section.grid+add; - }; - - $scope.scaleDown = function(section){ - var remove = (section.grid > 1) ? 1 : 0; - section.grid = section.grid-remove; - }; - - $scope.percentage = function(spans){ - return ((spans / $scope.columns) * 100).toFixed(8); - }; - - $scope.toggleCollection = function(collection, toggle){ - if(toggle){ - collection = []; - }else{ - delete collection; - } - }; - - - - /**************** - Section - *****************/ - $scope.configureSection = function(section, template){ - if(section === undefined){ - var space = ($scope.availableLayoutSpace > 4) ? 4 : $scope.availableLayoutSpace; - section = { - grid: space - }; - template.sections.push(section); - } - - $scope.currentSection = section; - }; - - $scope.deleteSection = function(section, template) { - if ($scope.currentSection === section) { - $scope.currentSection = undefined; - } - var index = template.sections.indexOf(section) - template.sections.splice(index, 1); - }; - - $scope.closeSection = function(){ - $scope.currentSection = undefined; - }; - - $scope.$watch("currentLayout", function(layout){ - if(layout){ - var total = 0; - _.forEach(layout.sections, function(section){ - total = (total + section.grid); - }); - - $scope.availableLayoutSpace = $scope.columns - total; - } - }, true); + * The controller for the content creation dialog + */ + function contentCreateController($scope, $routeParams, contentTypeResource, iconHelper, $location, navigationService, blueprintConfig) { + function initialize() { + contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { + $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); + }); + $scope.selectContentType = true; + $scope.selectBlueprint = false; + $scope.allowBlank = blueprintConfig.allowBlank; + } + function close() { + navigationService.hideMenu(); + } + function createBlank(docType) { + $location.path('/content/content/edit/' + $scope.currentNode.id).search('doctype=' + docType.alias + '&create=true'); + close(); + } + function createOrSelectBlueprintIfAny(docType) { + var blueprintIds = _.keys(docType.blueprints || {}); + $scope.docType = docType; + if (blueprintIds.length) { + if (blueprintConfig.skipSelect) { + createFromBlueprint(blueprintIds[0]); + } else { + $scope.selectContentType = false; + $scope.selectBlueprint = true; + } + } else { + createBlank(docType); + } + } + function createFromBlueprint(blueprintId) { + $location.path('/content/content/edit/' + $scope.currentNode.id).search('doctype=' + $scope.docType.alias + '&create=true&blueprintId=' + blueprintId); + close(); + } + $scope.createBlank = createBlank; + $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; + $scope.createFromBlueprint = createFromBlueprint; + initialize(); + } + angular.module('umbraco').controller('Umbraco.Editors.Content.CreateController', contentCreateController); + angular.module('umbraco').value('blueprintConfig', { + skipSelect: false, + allowBlank: true }); - -function RowConfigController($scope) { - - $scope.currentRow = $scope.model.currentRow; - $scope.editors = $scope.model.editors; - $scope.columns = $scope.model.columns; - - $scope.scaleUp = function(section, max, overflow) { - var add = 1; - if (overflow !== true) { - add = (max > 1) ? 1 : max; - } - //var add = (max > 1) ? 1 : max; - section.grid = section.grid + add; - }; - - $scope.scaleDown = function(section) { - var remove = (section.grid > 1) ? 1 : 0; - section.grid = section.grid - remove; - }; - - $scope.percentage = function(spans) { - return ((spans / $scope.columns) * 100).toFixed(8); - }; - - $scope.toggleCollection = function(collection, toggle) { - if (toggle) { - collection = []; - } - else { - delete collection; - } - }; - - - /**************** - area - *****************/ - $scope.configureCell = function(cell, row) { - if ($scope.currentCell && $scope.currentCell === cell) { - delete $scope.currentCell; - } - else { - if (cell === undefined) { - var available = $scope.availableRowSpace; - var space = 4; - - if (available < 4 && available > 0) { - space = available; - } - - cell = { - grid: space - }; - - row.areas.push(cell); - } - $scope.currentCell = cell; - } - }; - - $scope.deleteArea = function (cell, row) { - if ($scope.currentCell === cell) { - $scope.currentCell = undefined; - } - var index = row.areas.indexOf(cell) - row.areas.splice(index, 1); - }; - - $scope.closeArea = function() { - $scope.currentCell = undefined; - }; - - $scope.nameChanged = false; - var originalName = $scope.currentRow.name; - $scope.$watch("currentRow", function(row) { - if (row) { - - var total = 0; - _.forEach(row.areas, function(area) { - total = (total + area.grid); + (function () { + function CreateBlueprintController($scope, contentResource, notificationsService, navigationService, localizationService, formHelper, contentEditingHelper) { + $scope.message = { name: $scope.currentNode.name }; + var successText = {}; + localizationService.localize('blueprints_createBlueprintFrom', ['' + $scope.message.name + '']).then(function (localizedVal) { + $scope.title = localizedVal; }); - - $scope.availableRowSpace = $scope.columns - total; - - if (originalName) { - if (originalName != row.name) { - $scope.nameChanged = true; - } - else { - $scope.nameChanged = false; - } - } - } - }, true); - -} - -angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.RowConfigController", RowConfigController); - -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.Grid.EmbedController", - function ($scope, $rootScope, $timeout) { - - $scope.setEmbed = function(){ - $scope.embedDialog = {}; - $scope.embedDialog.view = "embed"; - $scope.embedDialog.show = true; - - $scope.embedDialog.submit = function(model) { - $scope.control.value = model.embed.preview; - $scope.embedDialog.show = false; - $scope.embedDialog = null; - }; - - $scope.embedDialog.close = function(oldModel) { - $scope.embedDialog.show = false; - $scope.embedDialog = null; + $scope.cancel = function () { + navigationService.hideMenu(); }; - - }; - - $timeout(function(){ - if($scope.control.$initializing){ - $scope.setEmbed(); - } - }, 200); -}); - -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.Grid.MacroController", - function ($scope, $rootScope, $timeout, dialogService, macroResource, macroService, $routeParams) { - - $scope.title = "Click to insert macro"; - - $scope.setMacro = function(){ - - var dialogData = { - richTextEditor: true, - macroData: $scope.control.value || { - macroAlias: $scope.control.editor.config && $scope.control.editor.config.macroAlias - ? $scope.control.editor.config.macroAlias : "" + $scope.create = function () { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: this.blueprintForm, + statusMessage: 'Creating blueprint...' + })) { + contentResource.createBlueprintFromContent($scope.currentNode.id, $scope.message.name).then(function (data) { + formHelper.resetForm({ + scope: $scope, + notifications: data.notifications + }); + navigationService.hideMenu(); + }, function (err) { + contentEditingHelper.handleSaveError({ + redirectOnFailure: false, + err: err + }); + }); } }; - - $scope.macroPickerOverlay = {}; - $scope.macroPickerOverlay.view = "macropicker"; - $scope.macroPickerOverlay.dialogData = dialogData; - $scope.macroPickerOverlay.show = true; - - $scope.macroPickerOverlay.submit = function(model) { - - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); - - $scope.control.value = { - macroAlias: macroObject.macroAlias, - macroParamsDictionary: macroObject.macroParamsDictionary - }; - - $scope.setPreview($scope.control.value ); - - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - $scope.macroPickerOverlay.close = function(oldModel) { - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - }; - - $scope.setPreview = function(macro){ - var contentId = $routeParams.id; - - macroResource.getMacroResultAsHtmlForEditor(macro.macroAlias, contentId, macro.macroParamsDictionary) - .then(function (htmlResult) { - $scope.title = macro.macroAlias; - if(htmlResult.trim().length > 0 && htmlResult.indexOf("Macro:") < 0){ - $scope.preview = htmlResult; + } + angular.module('umbraco').controller('Umbraco.Editors.Content.CreateBlueprintController', CreateBlueprintController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.ContentDeleteController + * @function + * + * @description + * The controller for deleting content + */ + function ContentDeleteController($scope, contentResource, treeService, navigationService, editorState, $location, dialogService, notificationsService) { + $scope.performDelete = function () { + // stop from firing again on double-click + if ($scope.busy) { + return false; + } + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + $scope.busy = true; + contentResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + treeService.removeNode($scope.currentNode); + if (rootNode) { + //ensure the recycle bin has child nodes now + var recycleBin = treeService.getDescendantNode(rootNode, -20); + if (recycleBin) { + recycleBin.hasChildren = true; + } + } + //if the current edited item is the same one as we're deleting, we need to navigate elsewhere + if (editorState.current && editorState.current.id == $scope.currentNode.id) { + //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent + var location = '/content'; + if ($scope.currentNode.parentId.toString() !== '-1') + location = '/content/content/edit/' + $scope.currentNode.parentId; + $location.path(location); + } + navigationService.hideMenu(); + }, function (err) { + $scope.currentNode.loading = false; + $scope.busy = false; + //check if response is ysod + if (err.status && err.status >= 500) { + dialogService.ysodDialog(err); + } + if (err.data && angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } } }); - }; - - $timeout(function(){ - if($scope.control.$initializing){ - $scope.setMacro(); - }else if($scope.control.value){ - $scope.setPreview($scope.control.value); - } - }, 200); -}); - -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.Grid.MediaController", - function ($scope, $rootScope, $timeout, userService) { - - if (!$scope.model.config.startNodeId) { - userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaId; - }); - } - - $scope.setImage = function(){ - $scope.mediaPickerOverlay = {}; - $scope.mediaPickerOverlay.view = "mediapicker"; - $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; - $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; - $scope.mediaPickerOverlay.showDetails = true; - $scope.mediaPickerOverlay.disableFolderSelect = true; - $scope.mediaPickerOverlay.onlyImages = true; - $scope.mediaPickerOverlay.show = true; - - $scope.mediaPickerOverlay.submit = function(model) { - var selectedImage = model.selectedImages[0]; - - $scope.control.value = { - focalPoint: selectedImage.focalPoint, - id: selectedImage.id, - image: selectedImage.image, - altText: selectedImage.altText - }; - - $scope.setUrl(); - - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - }; - - $scope.mediaPickerOverlay.close = function(oldModel) { - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - }; + $scope.cancel = function () { + navigationService.hideDialog(); }; - - $scope.setUrl = function(){ - - if($scope.control.value.image){ - var url = $scope.control.value.image; - - if($scope.control.editor.config && $scope.control.editor.config.size){ - url += "?width=" + $scope.control.editor.config.size.width; - url += "&height=" + $scope.control.editor.config.size.height; - url += "&animationprocessmode=first"; - - if($scope.control.value.focalPoint){ - url += "¢er=" + $scope.control.value.focalPoint.top +"," + $scope.control.value.focalPoint.left; - url += "&mode=crop"; + } + angular.module('umbraco').controller('Umbraco.Editors.Content.DeleteController', ContentDeleteController); + /** + * @ngdoc controller + * @name Umbraco.Editors.Content.EditController + * @function + * + * @description + * The controller for the content editor + */ + function ContentEditController($scope, $routeParams, contentResource) { + function scaffoldEmpty() { + return contentResource.getScaffold($routeParams.id, $routeParams.doctype); + } + function scaffoldBlueprint() { + return contentResource.getBlueprintScaffold($routeParams.id, $routeParams.blueprintId); + } + $scope.contentId = $routeParams.id; + $scope.saveMethod = contentResource.save; + $scope.getMethod = contentResource.getById; + $scope.getScaffoldMethod = $routeParams.blueprintId ? scaffoldBlueprint : scaffoldEmpty; + $scope.page = $routeParams.page; + $scope.isNew = $routeParams.create; + } + angular.module('umbraco').controller('Umbraco.Editors.Content.EditController', ContentEditController); + /** + * @ngdoc controller + * @name Umbraco.Editors.Content.EmptyRecycleBinController + * @function + * + * @description + * The controller for deleting content + */ + function ContentEmptyRecycleBinController($scope, contentResource, treeService, navigationService, notificationsService, $route) { + $scope.busy = false; + $scope.performDelete = function () { + //(used in the UI) + $scope.busy = true; + $scope.currentNode.loading = true; + contentResource.emptyRecycleBin($scope.currentNode.id).then(function (result) { + $scope.busy = false; + $scope.currentNode.loading = false; + //show any notifications + if (angular.isArray(result.notifications)) { + for (var i = 0; i < result.notifications.length; i++) { + notificationsService.showNotification(result.notifications[i]); } } - - // set default size if no crop present (moved from the view) - if (url.indexOf('?') == -1) - { - url += "?width=800&upscale=false&animationprocessmode=false" - } - $scope.url = url; - } + treeService.removeChildNodes($scope.currentNode); + navigationService.hideMenu(); + //reload the current view + $route.reload(); + }); }; - - $timeout(function(){ - if($scope.control.$initializing){ - $scope.setImage(); - }else if($scope.control.value){ - $scope.setUrl(); + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Content.EmptyRecycleBinController', ContentEmptyRecycleBinController); + angular.module('umbraco').controller('Umbraco.Editors.Content.MoveController', function ($scope, userService, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { + var dialogOptions = $scope.dialogOptions; + var searchText = 'Search...'; + localizationService.localize('general_search').then(function (value) { + searchText = value + '...'; + }); + $scope.dialogTreeEventHandler = $({}); + $scope.busy = false; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + $scope.treeModel = { hideHeader: false }; + userService.getCurrentUser().then(function (userData) { + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + }); + var node = dialogOptions.currentNode; + function nodeSelectHandler(ev, args) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); } - }, 200); -}); - -(function() { - "use strict"; - - function GridRichTextEditorController($scope, tinyMceService, macroService) { - - var vm = this; - - vm.openLinkPicker = openLinkPicker; - vm.openMediaPicker = openMediaPicker; - vm.openMacroPicker = openMacroPicker; - vm.openEmbed = openEmbed; - - function openLinkPicker(editor, currentTarget, anchorElement) { - vm.linkPickerOverlay = { - view: "linkpicker", - currentTarget: currentTarget, - show: true, - submit: function(model) { - tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); - vm.linkPickerOverlay.show = false; - vm.linkPickerOverlay = null; - } - }; + eventsService.emit('editors.content.moveController.select', args); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; } - - function openMediaPicker(editor, currentTarget, userData) { - vm.mediaPickerOverlay = { - currentTarget: currentTarget, - onlyImages: true, - showDetails: true, - startNodeId: userData.startMediaId, - view: "mediapicker", - show: true, - submit: function(model) { - tinyMceService.insertMediaInEditor(editor, model.selectedImages[0]); - vm.mediaPickerOverlay.show = false; - vm.mediaPickerOverlay = null; - } - }; + function nodeExpandedHandler(ev, args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } } - - function openEmbed(editor) { - vm.embedOverlay = { - view: "embed", - show: true, - submit: function(model) { - tinyMceService.insertEmbeddedMediaInEditor(editor, model.embed.preview); - vm.embedOverlay.show = false; - vm.embedOverlay = null; + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + }; + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { + event: evt, + node: result + }); + }; + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + $scope.move = function () { + $scope.busy = true; + $scope.error = false; + contentResource.move({ + parentId: $scope.target.id, + id: node.id + }).then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currently edited content (note: this might not be the content that was moved!!) + navigationService.syncTree({ + tree: 'content', + path: path, + forceReload: true, + activate: false + }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ + tree: 'content', + path: activeNodePath, + forceReload: false, + activate: true + }); + } + }); + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } } - }; + }); + }; + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); + }); + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + function openMiniListView(node) { + $scope.miniListView = node; } - - function openMacroPicker(editor, dialogData) { - vm.macroPickerOverlay = { - view: "macropicker", - dialogData: dialogData, - show: true, - submit: function(model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); - tinyMceService.insertMacroInEditor(editor, macroObject, $scope); - vm.macroPickerOverlay.show = false; - vm.macroPickerOverlay = null; - } - }; + }); + /** + * @ngdoc controller + * @name Umbraco.Editors.Content.RecycleBinController + * @function + * + * @description + * Controls the recycle bin for content + * + */ + function ContentRecycleBinController($scope, $routeParams, contentResource, navigationService, localizationService) { + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.page = {}; + $scope.page.name = 'Recycle Bin'; + $scope.page.nameLocked = true; + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.listViewPath = null; + $routeParams.id = '-20'; + contentResource.getRecycleBin().then(function (result) { + //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a + // single property, so we'll extract that property (list view) and use it's data. + var listproperty = result.tabs[0].properties[0]; + _.each(listproperty.config, function (val, key) { + $scope.model.config[key] = val; + }); + $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; + }); + $scope.model = { + config: { + entityType: $routeParams.section, + layouts: [] + } + }; + // sync tree node + navigationService.syncTree({ + tree: 'content', + path: [ + '-1', + $routeParams.id + ], + forceReload: false + }); + localizePageName(); + function localizePageName() { + var pageName = 'general_recycleBin'; + localizationService.localize(pageName).then(function (value) { + $scope.page.name = value; + }); } - - - } - - angular.module("umbraco").controller("Umbraco.PropertyEditors.Grid.RichTextEditorController", GridRichTextEditorController); - -})(); - -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.Grid.TextStringController", - function ($scope, $rootScope, $timeout, dialogService) { - - - - }); - - -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.GridController", - function ($scope, $http, assetsService, localizationService, $rootScope, dialogService, gridService, mediaResource, imageHelper, $timeout, umbRequestHelper, angularHelper) { - - // Grid status variables - var placeHolder = ""; - var currentForm = angularHelper.getCurrentForm($scope); - - $scope.currentRow = null; - $scope.currentCell = null; - $scope.currentToolsControl = null; - $scope.currentControl = null; - $scope.openRTEToolbarId = null; - $scope.hasSettings = false; - $scope.showRowConfigurations = true; - $scope.sortMode = false; - $scope.reorderKey = "general_reorder"; - - // ********************************************* - // Sortable options - // ********************************************* - - var draggedRteSettings; - - $scope.sortableOptionsRow = { - distance: 10, - cursor: "move", - placeholder: "ui-sortable-placeholder", - handle: ".umb-row-title-bar", - helper: "clone", - forcePlaceholderSize: true, - tolerance: "pointer", - zIndex: 999999999999999999, - scrollSensitivity: 100, - cursorAt: { - top: 40, - left: 60 - }, - - sort: function (event, ui) { - /* prevent vertical scroll out of the screen */ - var max = $(".umb-grid").width() - 150; - if (parseInt(ui.helper.css("left")) > max) { - ui.helper.css({ "left": max + "px" }); - } - if (parseInt(ui.helper.css("left")) < 20) { - ui.helper.css({ "left": 20 }); - } - }, - - start: function (e, ui) { - - // Fade out row when sorting - ui.item.context.style.display = "block"; - ui.item.context.style.opacity = "0.5"; - - draggedRteSettings = {}; - ui.item.find(".mceNoEditor").each(function () { - // remove all RTEs in the dragged row and save their settings - var id = $(this).attr("id"); - draggedRteSettings[id] = _.findWhere(tinyMCE.editors, { id: id }).settings; - // tinyMCE.execCommand("mceRemoveEditor", false, id); - }); - }, - - stop: function (e, ui) { - - // Fade in row when sorting stops - ui.item.context.style.opacity = "1"; - - // reset all RTEs affected by the dragging - ui.item.parents(".umb-column").find(".mceNoEditor").each(function () { - var id = $(this).attr("id"); - draggedRteSettings[id] = draggedRteSettings[id] || _.findWhere(tinyMCE.editors, { id: id }).settings; - tinyMCE.execCommand("mceRemoveEditor", false, id); - tinyMCE.init(draggedRteSettings[id]); + angular.module('umbraco').controller('Umbraco.Editors.Content.RecycleBinController', ContentRecycleBinController); + angular.module('umbraco').controller('Umbraco.Editors.Content.RestoreController', function ($scope, relationResource, contentResource, navigationService, appState, treeService) { + var dialogOptions = $scope.dialogOptions; + var node = dialogOptions.currentNode; + $scope.error = null; + $scope.success = false; + relationResource.getByChildId(node.id, 'relateParentDocumentOnDelete').then(function (data) { + if (data.length == 0) { + $scope.success = false; + $scope.error = { + errorMsg: 'Cannot automatically restore this item', + data: { Message: 'There is no \'restore\' relation found for this node. Use the Move menu item to move it manually.' } + }; + return; + } + $scope.relation = data[0]; + if ($scope.relation.parentId == -1) { + $scope.target = { + id: -1, + name: 'Root' + }; + } else { + contentResource.getById($scope.relation.parentId).then(function (data) { + $scope.target = data; + }, function (err) { + $scope.success = false; + $scope.error = err; }); - currentForm.$setDirty(); } - }; - - var notIncludedRte = []; - var cancelMove = false; - - $scope.sortableOptionsCell = { - distance: 10, - cursor: "move", - placeholder: "ui-sortable-placeholder", - handle: ".umb-control-handle", - helper: "clone", - connectWith: ".umb-cell-inner", - forcePlaceholderSize: true, - tolerance: "pointer", - zIndex: 999999999999999999, - scrollSensitivity: 100, - cursorAt: { - top: 45, - left: 90 - }, - - sort: function (event, ui) { - - /* prevent vertical scroll out of the screen */ - var position = parseInt(ui.item.parent().offset().left) + parseInt(ui.helper.css("left")) - parseInt($(".umb-grid").offset().left); - var max = $(".umb-grid").width() - 220; - if (position > max) { - ui.helper.css({ "left": max - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" }); - } - if (position < 0) { - ui.helper.css({ "left": 0 - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" }); - } - }, - - over: function (event, ui) { - var allowedEditors = $(event.target).scope().area.allowed; - - if ($.inArray(ui.item.scope().control.editor.alias, allowedEditors) < 0 && allowedEditors) { - - $scope.$apply(function () { - $(event.target).scope().area.dropNotAllowed = true; - }); - - ui.placeholder.hide(); - cancelMove = true; - } - else { - if ($(event.target).scope().area.controls.length == 0){ - - $scope.$apply(function () { - $(event.target).scope().area.dropOnEmpty = true; + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + $scope.restore = function () { + // this code was copied from `content.move.controller.js` + contentResource.move({ + parentId: $scope.target.id, + id: node.id + }).then(function (path) { + $scope.success = true; + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + navigationService.syncTree({ + tree: 'content', + path: path, + forceReload: true, + activate: false + }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ + tree: 'content', + path: activeNodePath, + forceReload: false, + activate: true }); - ui.placeholder.hide(); - } else { - ui.placeholder.show(); } - cancelMove = false; - } - }, - - out: function(event, ui) { - $scope.$apply(function () { - $(event.target).scope().area.dropNotAllowed = false; - $(event.target).scope().area.dropOnEmpty = false; }); - }, - - update: function (event, ui) { - /* add all RTEs which are affected by the dragging */ - if (!ui.sender) { - if (cancelMove) { - ui.item.sortable.cancel(); + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + }; + }); + (function () { + 'use strict'; + function ContentRightsController($scope, $timeout, contentResource, localizationService, angularHelper) { + var vm = this; + var currentForm; + vm.availableUserGroups = []; + vm.selectedUserGroups = []; + vm.removedUserGroups = []; + vm.viewState = 'manageGroups'; + vm.labels = {}; + vm.showNotification = false; + vm.setViewSate = setViewSate; + vm.editPermissions = editPermissions; + vm.setPermissions = setPermissions; + vm.save = save; + vm.removePermissions = removePermissions; + vm.cancelManagePermissions = cancelManagePermissions; + vm.closeDialog = closeDialog; + vm.stay = stay; + function onInit() { + vm.loading = true; + contentResource.getDetailedPermissions($scope.currentNode.id).then(function (userGroups) { + initData(userGroups); + vm.loading = false; + currentForm = angularHelper.getCurrentForm($scope); + }); + } + /** + * This will initialize the data and set the correct selectedUserGroups based on the default permissions and explicit permissions assigned + * @param {any} userGroups + */ + function initData(userGroups) { + //reset this + vm.selectedUserGroups = []; + vm.availableUserGroups = userGroups; + angular.forEach(vm.availableUserGroups, function (group) { + if (group.permissions) { + //if there's explicit permissions assigned than it's selected + assignGroupPermissions(group); } - ui.item.parents(".umb-cell.content").find(".mceNoEditor").each(function () { - if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { - notIncludedRte.splice(0, 0, $(this).attr("id")); + }); + } + function setViewSate(state) { + vm.viewState = state; + } + function editPermissions(group) { + vm.selectedUserGroup = group; + if (!vm.selectedUserGroup.permissions) { + //if no permissions are explicitly set this means we need to show the defaults + vm.selectedUserGroup.permissions = vm.selectedUserGroup.defaultPermissions; + } + localizationService.localize('defaultdialogs_permissionsSetForGroup', [ + $scope.currentNode.name, + vm.selectedUserGroup.name + ]).then(function (value) { + vm.labels.permissionsSetForGroup = value; + }); + setViewSate('managePermissions'); + } + function assignGroupPermissions(group) { + // clear allowed permissions before we make the list so we don't have duplicates + group.allowedPermissions = []; + // get list of checked permissions + angular.forEach(group.permissions, function (permissionGroup) { + angular.forEach(permissionGroup, function (permission) { + if (permission.checked) { + //the `allowedPermissions` is what will get sent up to the server for saving + group.allowedPermissions.push(permission); } }); + }); + if (!group.selected) { + // set to selected so we can remove from the dropdown easily + group.selected = true; + vm.selectedUserGroups.push(group); + //remove from the removed groups if it's been re-added + vm.removedUserGroups = _.reject(vm.removedUserGroups, function (g) { + return g.id == group.id; + }); } - else { - $(event.target).find(".mceNoEditor").each(function () { - if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { - notIncludedRte.splice(0, 0, $(this).attr("id")); + } + function setPermissions(group) { + assignGroupPermissions(group); + setViewSate('manageGroups'); + } + /** + * This essentially resets the permissions for a group for this content item, it will remove it from the selected list + * @param {any} index + */ + function removePermissions(index) { + // remove as selected so we can select it from the dropdown again + var group = vm.selectedUserGroups[index]; + group.selected = false; + //reset assigned permissions - so it will default back to default permissions + group.permissions = []; + group.allowedPermissions = []; + vm.selectedUserGroups.splice(index, 1); + //track it in the removed so this gets pushed to the server + vm.removedUserGroups.push(group); + } + function cancelManagePermissions() { + setViewSate('manageGroups'); + } + function formatSaveModel(permissionsSave, groupCollection) { + angular.forEach(groupCollection, function (g) { + permissionsSave[g.id] = []; + angular.forEach(g.allowedPermissions, function (p) { + permissionsSave[g.id].push(p.permissionCode); + }); + }); + } + function save() { + vm.saveState = 'busy'; + vm.saveError = false; + vm.saveSuccces = false; + //this is a dictionary that we need to populate + var permissionsSave = {}; + //format the selectedUserGroups, then the removedUserGroups since we want to pass data from both collections up + formatSaveModel(permissionsSave, vm.selectedUserGroups); + formatSaveModel(permissionsSave, vm.removedUserGroups); + var saveModel = { + contentId: $scope.currentNode.id, + permissions: permissionsSave + }; + contentResource.savePermissions(saveModel).then(function (userGroups) { + //re-assign model from server since it could have changed + initData(userGroups); + // clear dirty state on the form so we don't see the discard changes notification + // we use a timeout here because in some cases the initData reformats the userGroups model and triggers a change after the form state was changed + $timeout(function () { + if (currentForm) { + currentForm.$dirty = false; } }); - } - currentForm.$setDirty(); - }, - - start: function (e, ui) { - - // fade out control when sorting - ui.item.context.style.display = "block"; - ui.item.context.style.opacity = "0.5"; - - // reset dragged RTE settings in case a RTE isn't dragged - draggedRteSettings = undefined; - ui.item.context.style.display = "block"; - ui.item.find(".mceNoEditor").each(function () { - notIncludedRte = []; - var editors = _.findWhere(tinyMCE.editors, { id: $(this).attr("id") }); - - // save the dragged RTE settings - if(editors) { - draggedRteSettings = editors.settings; - - // remove the dragged RTE - tinyMCE.execCommand("mceRemoveEditor", false, $(this).attr("id")); - - } - + vm.saveState = 'success'; + vm.saveSuccces = true; + }, function (error) { + vm.saveState = 'error'; + vm.saveError = error; }); - }, - - stop: function (e, ui) { - - // Fade in control when sorting stops - ui.item.context.style.opacity = "1"; - - ui.item.parents(".umb-cell-content").find(".mceNoEditor").each(function () { - if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { - // add all dragged's neighbouring RTEs in the new cell - notIncludedRte.splice(0, 0, $(this).attr("id")); - } + } + function stay() { + vm.showNotification = false; + } + function closeDialog() { + // check if form has been changed. If it has show discard changes notification + if (currentForm && currentForm.$dirty) { + vm.showNotification = true; + } else { + $scope.nav.hideDialog(); + } + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Editors.Content.RightsController', ContentRightsController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.ContentBlueprint.CreateController + * @function + * + * @description + * The controller for creating content blueprints + */ + function ContentBlueprintCreateController($scope, $location, contentTypeResource, navigationService) { + var vm = this; + var node = $scope.dialogOptions.currentNode; + vm.createBlueprint = createBlueprint; + function onInit() { + vm.loading = true; + contentTypeResource.getAll().then(function (documentTypes) { + vm.documentTypes = documentTypes; + vm.loading = false; + }); + } + function createBlueprint(documentType) { + $location.path('/settings/contentBlueprints/edit/' + node.id).search('create', 'true').search('doctype', documentType.alias); + navigationService.hideMenu(); + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Editors.ContentBlueprint.CreateController', ContentBlueprintCreateController); + /** + * @ngdoc controller + * @name Umbraco.Editors.ContentBlueprint.DeleteController + * @function + * + * @description + * The controller for deleting content blueprints + */ + function ContentBlueprintDeleteController($scope, contentResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + contentResource.deleteBlueprint($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.ContentBlueprint.DeleteController', ContentBlueprintDeleteController); + /** + * @ngdoc controller + * @name Umbraco.Editors.Content.EditController + * @function + * + * @description + * The controller for the content editor + */ + function ContentBlueprintEditController($scope, $routeParams, contentResource) { + var excludedProps = [ + '_umb_urls', + '_umb_releasedate', + '_umb_expiredate', + '_umb_template' + ]; + function getScaffold() { + return contentResource.getScaffold(-1, $routeParams.doctype).then(function (scaffold) { + var lastTab = scaffold.tabs[scaffold.tabs.length - 1]; + lastTab.properties = _.filter(lastTab.properties, function (p) { + return excludedProps.indexOf(p.alias) === -1; }); - $timeout(function () { - // reconstruct the dragged RTE (could be undefined when dragging something else than RTE) - if (draggedRteSettings !== undefined) { - tinyMCE.init(draggedRteSettings); - } - - _.forEach(notIncludedRte, function (id) { - // reset all the other RTEs - if (draggedRteSettings === undefined || id !== draggedRteSettings.id) { - var rteSettings = _.findWhere(tinyMCE.editors, { id: id }).settings; - tinyMCE.execCommand("mceRemoveEditor", false, id); - tinyMCE.init(rteSettings); - } - }); - }, 500, false); - - $scope.$apply(function () { - - var cell = $(e.target).scope().area; - cell.hasActiveChild = hasActiveChild(cell, cell.controls); - cell.active = false; + scaffold.allowPreview = false; + scaffold.allowedActions = [ + 'A', + 'S', + 'C' + ]; + return scaffold; + }); + } + $scope.contentId = $routeParams.id; + $scope.isNew = $routeParams.id === '-1'; + $scope.saveMethod = contentResource.saveBlueprint; + $scope.getMethod = contentResource.getBlueprintById; + $scope.getScaffoldMethod = getScaffold; + } + angular.module('umbraco').controller('Umbraco.Editors.ContentBlueprint.EditController', ContentBlueprintEditController); + function startUpVideosDashboardController($scope, xmlhelper, $log, $http) { + $scope.videos = []; + $scope.init = function (url) { + var proxyUrl = 'dashboard/feedproxy.aspx?url=' + url; + $http.get(proxyUrl).then(function (data) { + var feed = $(data.data); + $('item', feed).each(function (i, item) { + var video = {}; + video.thumbnail = $(item).find('thumbnail').attr('url'); + video.title = $('title', item).text(); + video.link = $('guid', item).text(); + $scope.videos.push(video); }); - } - + }); }; - - $scope.toggleSortMode = function() { - $scope.sortMode = !$scope.sortMode; - if($scope.sortMode) { - $scope.reorderKey = "general_reorderDone"; + } + angular.module('umbraco').controller('Umbraco.Dashboard.StartupVideosController', startUpVideosDashboardController); + function startUpDynamicContentController(dashboardResource, assetsService) { + var vm = this; + vm.loading = true; + vm.showDefault = false; + // default dashboard content + vm.defaultDashboard = { + infoBoxes: [ + { + title: 'Documentation', + description: 'Find the answers to your Umbraco questions', + url: 'https://our.umbraco.org/documentation/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=documentation/' + }, + { + title: 'Community', + description: 'Find the answers or ask your Umbraco questions', + url: 'https://our.umbraco.org/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=our_forum' + }, + { + title: 'Umbraco.tv', + description: 'Tutorial videos (some are free, some are on subscription)', + url: 'https://umbraco.tv/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=tutorial_videos' + }, + { + title: 'Training', + description: 'Real-life training and official Umbraco certifications', + url: 'https://umbraco.com/training/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=training' + } + ], + articles: [ + { + title: 'Umbraco.TV - Learn from the source!', + description: 'Umbraco.TV will help you go from zero to Umbraco hero at a pace that suits you. Our easy to follow online training videos will give you the fundamental knowledge to start building awesome Umbraco websites.', + img: 'views/dashboard/default/umbracotv.jpg', + url: 'https://umbraco.tv/?utm_source=core&utm_medium=dashboard&utm_content=image&utm_campaign=tv', + altText: 'Umbraco.TV - Hours of Umbraco Video Tutorials', + buttonText: 'Visit Umbraco.TV' + }, + { + title: 'Our Umbraco - The Friendliest Community', + description: 'Our Umbraco - the official community site is your one stop for everything Umbraco. Whether you need a question answered or looking for cool plugins, the world\'s best and friendliest community is just a click away.', + img: 'views/dashboard/default/ourumbraco.jpg', + url: 'https://our.umbraco.org/?utm_source=core&utm_medium=dashboard&utm_content=image&utm_campaign=our', + altText: 'Our Umbraco', + buttonText: 'Visit Our Umbraco' + } + ] + }; + //proxy remote css through the local server + assetsService.loadCss(dashboardResource.getRemoteDashboardCssUrl('content')); + dashboardResource.getRemoteDashboardContent('content').then(function (data) { + vm.loading = false; + //test if we have received valid data + //we capture it like this, so we avoid UI errors - which automatically triggers ui based on http response code + if (data && data.sections) { + vm.dashboard = data; } else { - $scope.reorderKey = "general_reorder"; + vm.showDefault = true; } + }, function (exception) { + console.error(exception); + vm.loading = false; + vm.showDefault = true; + }); + } + angular.module('umbraco').controller('Umbraco.Dashboard.StartUpDynamicContentController', startUpDynamicContentController); + function FormsController($scope, $route, $cookieStore, packageResource, localizationService) { + $scope.installForms = function () { + $scope.state = localizationService.localize('packager_installStateDownloading'); + packageResource.fetch('CD44CF39-3D71-4C19-B6EE-948E1FAF0525').then(function (pack) { + $scope.state = localizationService.localize('packager_installStateImporting'); + return packageResource.import(pack); + }, $scope.error).then(function (pack) { + $scope.state = localizationService.localize('packager_installStateInstalling'); + return packageResource.installFiles(pack); + }, $scope.error).then(function (pack) { + $scope.state = localizationService.localize('packager_installStateRestarting'); + return packageResource.installData(pack); + }, $scope.error).then(function (pack) { + $scope.state = localizationService.localize('packager_installStateComplete'); + return packageResource.cleanUp(pack); + }, $scope.error).then($scope.complete, $scope.error); }; - - $scope.showReorderButton = function() { - if($scope.model.value && $scope.model.value.sections) { - for(var i = 0; $scope.model.value.sections.length > i; i++) { - var section = $scope.model.value.sections[i]; - if(section.rows && section.rows.length > 0) { - return true; - } - } - } + $scope.complete = function (result) { + var url = window.location.href + '?init=true'; + $cookieStore.put('umbPackageInstallId', result.packageGuid); + window.location.reload(true); }; - - // ********************************************* - // Add items overlay menu - // ********************************************* - $scope.openEditorOverlay = function(event, area, index, key) { - $scope.editorOverlay = { - view: "itempicker", - filter: false, - title: localizationService.localize("grid_insertControl"), - availableItems: area.$allowedEditors, - event: event, - show: true, - submit: function(model) { - $scope.addControl(model.selectedItem, area, index); - $scope.editorOverlay.show = false; - $scope.editorOverlay = null; - } - }; - }; - - // ********************************************* - // Template management functions - // ********************************************* - - $scope.addTemplate = function (template) { - $scope.model.value = angular.copy(template); - - //default row data - _.forEach($scope.model.value.sections, function (section) { - $scope.initSection(section); - }); + $scope.error = function (err) { + $scope.state = undefined; + $scope.error = err; + //This will return a rejection meaning that the promise change above will stop + return $q.reject(); }; - - - // ********************************************* - // Row management function - // ********************************************* - - $scope.clickRow = function(index, rows) { - rows[index].active = true; + function Video_player(videoId) { + // Get dom elements + this.container = document.getElementById(videoId); + this.video = this.container.getElementsByTagName('video')[0]; + //Create controls + this.controls = document.createElement('div'); + this.controls.className = 'video-controls'; + this.seek_bar = document.createElement('input'); + this.seek_bar.className = 'seek-bar'; + this.seek_bar.type = 'range'; + this.seek_bar.setAttribute('value', '0'); + this.loader = document.createElement('div'); + this.loader.className = 'loader'; + this.progress_bar = document.createElement('span'); + this.progress_bar.className = 'progress-bar'; + // Insert controls + this.controls.appendChild(this.seek_bar); + this.container.appendChild(this.controls); + this.controls.appendChild(this.loader); + this.loader.appendChild(this.progress_bar); + } + Video_player.prototype.seeking = function () { + // get the value of the seekbar (hidden input[type="range"]) + var time = this.video.duration * (this.seek_bar.value / 100); + // Update video to seekbar value + this.video.currentTime = time; }; - - $scope.clickOutsideRow = function(index, rows) { - rows[index].active = false; + // Stop video when user initiates seeking + Video_player.prototype.start_seek = function () { + this.video.pause(); }; - - function getAllowedLayouts(section) { - - var layouts = $scope.model.config.items.layouts; - - //This will occur if it is a new section which has been - // created from a 'template' - if (section.allowed && section.allowed.length > 0) { - return _.filter(layouts, function (layout) { - return _.indexOf(section.allowed, layout.name) >= 0; + // Start video when user stops seeking + Video_player.prototype.stop_seek = function () { + this.video.play(); + }; + // Update the progressbar (span.loader) according to video.currentTime + Video_player.prototype.update_progress_bar = function () { + // Get video progress in % + var value = 100 / this.video.duration * this.video.currentTime; + // Update progressbar + this.progress_bar.style.width = value + '%'; + }; + // Bind progressbar to mouse when seeking + Video_player.prototype.handle_mouse_move = function (event) { + // Get position of progressbar relative to browser window + var pos = this.progress_bar.getBoundingClientRect().left; + // Make sure event is reckonized cross-browser + event = event || window.event; + // Update progressbar + this.progress_bar.style.width = event.clientX - pos + 'px'; + }; + // Eventlisteners for seeking + Video_player.prototype.video_event_handler = function (videoPlayer, interval) { + // Update the progress bar + var animate_progress_bar = setInterval(function () { + videoPlayer.update_progress_bar(); + }, interval); + // Fire when input value changes (user seeking) + videoPlayer.seek_bar.addEventListener('change', function () { + videoPlayer.seeking(); + }); + // Fire when user clicks on seekbar + videoPlayer.seek_bar.addEventListener('mousedown', function (clickEvent) { + // Pause video playback + videoPlayer.start_seek(); + // Stop updating progressbar according to video progress + clearInterval(animate_progress_bar); + // Update progressbar to where user clicks + videoPlayer.handle_mouse_move(clickEvent); + // Bind progressbar to cursor + window.onmousemove = function (moveEvent) { + videoPlayer.handle_mouse_move(moveEvent); + }; + }); + // Fire when user releases seekbar + videoPlayer.seek_bar.addEventListener('mouseup', function () { + // Unbind progressbar from cursor + window.onmousemove = null; + // Start video playback + videoPlayer.stop_seek(); + // Animate the progressbar + animate_progress_bar = setInterval(function () { + videoPlayer.update_progress_bar(); + }, interval); + }); + }; + var videoPlayer = new Video_player('video_1'); + videoPlayer.video_event_handler(videoPlayer, 17); + } + angular.module('umbraco').controller('Umbraco.Dashboard.FormsDashboardController', FormsController); + function startupLatestEditsController($scope) { + } + angular.module('umbraco').controller('Umbraco.Dashboard.StartupLatestEditsController', startupLatestEditsController); + function MediaFolderBrowserDashboardController($rootScope, $scope, $location, contentTypeResource, userService) { + var currentUser = {}; + userService.getCurrentUser().then(function (user) { + currentUser = user; + // check if the user has access to the root which they will require to see this dashboard + if (currentUser.startMediaIds.indexOf(-1) >= 0) { + //get the system media listview + contentTypeResource.getPropertyTypeScaffold(-96).then(function (dt) { + $scope.fakeProperty = { + alias: 'contents', + config: dt.config, + description: '', + editor: dt.editor, + hideLabel: true, + id: 1, + label: 'Contents:', + validation: { + mandatory: false, + pattern: null + }, + value: '', + view: dt.view + }; }); + } else if (currentUser.startMediaIds.length > 0) { + // redirect to start node + $location.path('/media/media/edit/' + (currentUser.startMediaIds.length === 0 ? -1 : currentUser.startMediaIds[0])); } - else { - - - return layouts; - } + }); + } + angular.module('umbraco').controller('Umbraco.Dashboard.MediaFolderBrowserDashboardController', MediaFolderBrowserDashboardController); + function ExamineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeout) { + $scope.indexerDetails = []; + $scope.searcherDetails = []; + $scope.loading = true; + function checkProcessing(indexer, checkActionName) { + umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', checkActionName, { indexerName: indexer.name })), 'Failed to check index processing').then(function (data) { + if (data !== null && data !== 'null') { + //copy all resulting properties + for (var k in data) { + indexer[k] = data[k]; + } + indexer.isProcessing = false; + } else { + $timeout(function () { + //don't continue if we've tried 100 times + if (indexer.processingAttempts < 100) { + checkProcessing(indexer, checkActionName); + //add an attempt + indexer.processingAttempts++; + } else { + //we've exceeded 100 attempts, stop processing + indexer.isProcessing = false; + } + }, 1000); + } + }); } - - $scope.addRow = function (section, layout) { - - //copy the selected layout into the rows collection - var row = angular.copy(layout); - - // Init row value - row = $scope.initRow(row); - - // Push the new row - if (row) { - section.rows.push(row); + $scope.search = function (searcher, e) { + if (e && e.keyCode !== 13) { + return; } - - currentForm.$setDirty(); - - $scope.showRowConfigurations = false; - + umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'GetSearchResults', { + searcherName: searcher.name, + query: encodeURIComponent(searcher.searchText), + queryType: searcher.searchType + })), 'Failed to search').then(function (searchResults) { + searcher.isSearching = true; + searcher.searchResults = searchResults; + }); }; - - $scope.removeRow = function (section, $index) { - if (section.rows.length > 0) { - section.rows.splice($index, 1); - $scope.currentRow = null; - $scope.openRTEToolbarId = null; - currentForm.$setDirty(); - } - - if(section.rows.length === 0) { - $scope.showRowConfigurations = true; + $scope.toggle = function (provider, propName) { + if (provider[propName] !== undefined) { + provider[propName] = !provider[propName]; + } else { + provider[propName] = true; } }; - - var shouldApply = function(item, itemType, gridItem) { - if (item.applyTo === undefined || item.applyTo === null || item.applyTo === "") { - return true; + $scope.rebuildIndex = function (indexer) { + if (confirm('This will cause the index to be rebuilt. ' + 'Depending on how much content there is in your site this could take a while. ' + 'It is not recommended to rebuild an index during times of high website traffic ' + 'or when editors are editing content.')) { + indexer.isProcessing = true; + indexer.processingAttempts = 0; + umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'PostRebuildIndex', { indexerName: indexer.name })), 'Failed to rebuild index').then(function () { + //rebuilding has started, nothing is returned accept a 200 status code. + //lets poll to see if it is done. + $timeout(function () { + checkProcessing(indexer, 'PostCheckRebuildIndex'); + }, 1000); + }); } - - if (typeof (item.applyTo) === "string") { - return item.applyTo === itemType; + }; + $scope.optimizeIndex = function (indexer) { + if (confirm('This will cause the index to be optimized which will improve its performance. ' + 'It is not recommended to optimize an index during times of high website traffic ' + 'or when editors are editing content.')) { + indexer.isProcessing = true; + umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'PostOptimizeIndex', { indexerName: indexer.name })), 'Failed to optimize index').then(function () { + //optimizing has started, nothing is returned accept a 200 status code. + //lets poll to see if it is done. + $timeout(function () { + checkProcessing(indexer, 'PostCheckOptimizeIndex'); + }, 1000); + }); } - - if (itemType === "row") { - if (item.applyTo.row === undefined) { - return false; - } - if (item.applyTo.row === null || item.applyTo.row === "") { - return true; + }; + $scope.closeSearch = function (searcher) { + searcher.isSearching = true; + }; + //go get the data + //combine two promises and execute when they are both done + $q.all([ + //get the indexer details + umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'GetIndexerDetails')), 'Failed to retrieve indexer details').then(function (data) { + $scope.indexerDetails = data; + }), + //get the searcher details + umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'GetSearcherDetails')), 'Failed to retrieve searcher details').then(function (data) { + $scope.searcherDetails = data; + for (var s in $scope.searcherDetails) { + $scope.searcherDetails[s].searchType = 'text'; } - var rows = item.applyTo.row.split(','); - return _.indexOf(rows, gridItem.name) !== -1; - } else if (itemType === "cell") { - if (item.applyTo.cell === undefined) { - return false; + }) + ]).then(function () { + //all init loading is complete + $scope.loading = false; + }); + } + angular.module('umbraco').controller('Umbraco.Dashboard.ExamineMgmtController', ExamineMgmtController); + (function () { + 'use strict'; + function HealthCheckController($scope, healthCheckResource) { + var SUCCESS = 0; + var WARNING = 1; + var ERROR = 2; + var INFO = 3; + var vm = this; + vm.viewState = 'list'; + vm.groups = []; + vm.selectedGroup = {}; + vm.getStatus = getStatus; + vm.executeAction = executeAction; + vm.checkAllGroups = checkAllGroups; + vm.checkAllInGroup = checkAllInGroup; + vm.openGroup = openGroup; + vm.setViewState = setViewState; + // Get a (grouped) list of all health checks + healthCheckResource.getAllChecks().then(function (response) { + vm.groups = response; + }); + function setGroupGlobalResultType(group) { + var totalSuccess = 0; + var totalError = 0; + var totalWarning = 0; + var totalInfo = 0; + // count total number of statusses + angular.forEach(group.checks, function (check) { + angular.forEach(check.status, function (status) { + switch (status.resultType) { + case SUCCESS: + totalSuccess = totalSuccess + 1; + break; + case WARNING: + totalWarning = totalWarning + 1; + break; + case ERROR: + totalError = totalError + 1; + break; + case INFO: + totalInfo = totalInfo + 1; + break; + } + }); + }); + group.totalSuccess = totalSuccess; + group.totalError = totalError; + group.totalWarning = totalWarning; + group.totalInfo = totalInfo; + } + // Get the status of an individual check + function getStatus(check) { + check.loading = true; + check.status = null; + healthCheckResource.getStatus(check.id).then(function (response) { + check.loading = false; + check.status = response; + }); + } + function executeAction(check, index, action) { + check.loading = true; + healthCheckResource.executeAction(action).then(function (response) { + check.status[index] = response; + check.loading = false; + }); + } + function checkAllGroups(groups) { + // set number of checks which has been executed + for (var i = 0; i < groups.length; i++) { + var group = groups[i]; + checkAllInGroup(group, group.checks); } - if (item.applyTo.cell === null || item.applyTo.cell === "") { - return true; + vm.groups = groups; + } + function checkAllInGroup(group, checks) { + group.checkCounter = 0; + group.loading = true; + angular.forEach(checks, function (check) { + check.loading = true; + healthCheckResource.getStatus(check.id).then(function (response) { + check.status = response; + group.checkCounter = group.checkCounter + 1; + check.loading = false; + // when all checks are done, set global group result + if (group.checkCounter === checks.length) { + setGroupGlobalResultType(group); + group.loading = false; + } + }); + }); + } + function openGroup(group) { + vm.selectedGroup = group; + vm.viewState = 'details'; + } + function setViewState(state) { + vm.viewState = state; + if (state === 'list') { + for (var i = 0; i < vm.groups.length; i++) { + var group = vm.groups[i]; + setGroupGlobalResultType(group); + } } - var cells = item.applyTo.cell.split(','); - var cellSize = gridItem.grid.toString(); - return _.indexOf(cells, cellSize) !== -1; } } - - $scope.editGridItemSettings = function (gridItem, itemType) { - - placeHolder = "{0}"; - - var styles, config; - if (itemType === 'control') { - styles = null; - config = angular.copy(gridItem.editor.config.settings); - } else { - styles = _.filter(angular.copy($scope.model.config.items.styles), function (item) { return shouldApply(item, itemType, gridItem); }); - config = _.filter(angular.copy($scope.model.config.items.config), function (item) { return shouldApply(item, itemType, gridItem); }); + angular.module('umbraco').controller('Umbraco.Dashboard.HealthCheckController', HealthCheckController); + }()); + (function () { + 'use strict'; + function RedirectUrlsController($scope, redirectUrlsResource, notificationsService, localizationService, $q) { + //...todo + //search by url or url part + //search by domain + //display domain in dashboard results? + //used to cancel any request in progress if another one needs to take it's place + var vm = this; + var canceler = null; + vm.dashboard = { + searchTerm: '', + loading: false, + urlTrackerDisabled: false, + userIsAdmin: false + }; + vm.pagination = { + pageIndex: 0, + pageNumber: 1, + totalPages: 1, + pageSize: 20 + }; + vm.goToPage = goToPage; + vm.search = search; + vm.removeRedirect = removeRedirect; + vm.disableUrlTracker = disableUrlTracker; + vm.enableUrlTracker = enableUrlTracker; + vm.filter = filter; + vm.checkEnabled = checkEnabled; + function activate() { + vm.checkEnabled().then(function () { + vm.search(); + }); } - - if(angular.isObject(gridItem.config)){ - _.each(config, function(cfg){ - var val = gridItem.config[cfg.key]; - if(val){ - cfg.value = stripModifier(val, cfg.modifier); - } + function checkEnabled() { + vm.dashboard.loading = true; + return redirectUrlsResource.getEnableState().then(function (response) { + vm.dashboard.urlTrackerDisabled = response.enabled !== true; + vm.dashboard.userIsAdmin = response.userIsAdmin; + vm.dashboard.loading = false; }); } - - if(angular.isObject(gridItem.styles)){ - _.each(styles, function(style){ - var val = gridItem.styles[style.key]; - if(val){ - style.value = stripModifier(val, style.modifier); + function goToPage(pageNumber) { + vm.pagination.pageIndex = pageNumber - 1; + vm.pagination.pageNumber = pageNumber; + vm.search(); + } + function search() { + vm.dashboard.loading = true; + var searchTerm = vm.dashboard.searchTerm; + if (searchTerm === undefined) { + searchTerm = ''; + } + redirectUrlsResource.searchRedirectUrls(searchTerm, vm.pagination.pageIndex, vm.pagination.pageSize).then(function (response) { + vm.redirectUrls = response.searchResults; + // update pagination + vm.pagination.pageIndex = response.currentPage; + vm.pagination.pageNumber = response.currentPage + 1; + vm.pagination.totalPages = response.pageCount; + vm.dashboard.loading = false; + }); + } + function removeRedirect(redirectToDelete) { + localizationService.localize('redirectUrls_confirmRemove', [ + redirectToDelete.originalUrl, + redirectToDelete.destinationUrl + ]).then(function (value) { + var toggleConfirm = confirm(value); + if (toggleConfirm) { + redirectUrlsResource.deleteRedirectUrl(redirectToDelete.redirectId).then(function () { + var index = vm.redirectUrls.indexOf(redirectToDelete); + vm.redirectUrls.splice(index, 1); + notificationsService.success(localizationService.localize('redirectUrls_redirectRemoved')); + // check if new redirects needs to be loaded + if (vm.redirectUrls.length === 0 && vm.pagination.totalPages > 1) { + // if we are not on the first page - get records from the previous + if (vm.pagination.pageIndex > 0) { + vm.pagination.pageIndex = vm.pagination.pageIndex - 1; + vm.pagination.pageNumber = vm.pagination.pageNumber - 1; + } + search(); + } + }, function (error) { + notificationsService.error(localizationService.localize('redirectUrls_redirectRemoveError')); + }); } }); } - - $scope.gridItemSettingsDialog = {}; - $scope.gridItemSettingsDialog.view = "views/propertyeditors/grid/dialogs/config.html"; - $scope.gridItemSettingsDialog.title = "Settings"; - $scope.gridItemSettingsDialog.styles = styles; - $scope.gridItemSettingsDialog.config = config; - - $scope.gridItemSettingsDialog.show = true; - - $scope.gridItemSettingsDialog.submit = function(model) { - - var styleObject = {}; - var configObject = {}; - - _.each(model.styles, function(style){ - if(style.value){ - styleObject[style.key] = addModifier(style.value, style.modifier); + function disableUrlTracker() { + localizationService.localize('redirectUrls_confirmDisable').then(function (value) { + var toggleConfirm = confirm(value); + if (toggleConfirm) { + redirectUrlsResource.toggleUrlTracker(true).then(function () { + activate(); + notificationsService.success(localizationService.localize('redirectUrls_disabledConfirm')); + }, function (error) { + notificationsService.warning(localizationService.localize('redirectUrls_disableError')); + }); } }); - _.each(model.config, function (cfg) { - if (cfg.value) { - configObject[cfg.key] = addModifier(cfg.value, cfg.modifier); - } + } + function enableUrlTracker() { + redirectUrlsResource.toggleUrlTracker(false).then(function () { + activate(); + notificationsService.success(localizationService.localize('redirectUrls_enabledConfirm')); + }, function (error) { + notificationsService.warning(localizationService.localize('redirectUrls_enableError')); }); - - gridItem.styles = styleObject; - gridItem.config = configObject; - gridItem.hasConfig = gridItemHasConfig(styleObject, configObject); - - currentForm.$setDirty(); - - $scope.gridItemSettingsDialog.show = false; - $scope.gridItemSettingsDialog = null; - }; - - $scope.gridItemSettingsDialog.close = function(oldModel) { - $scope.gridItemSettingsDialog.show = false; - $scope.gridItemSettingsDialog = null; - }; - - }; - - function stripModifier(val, modifier) { - if (!val || !modifier || modifier.indexOf(placeHolder) < 0) { - return val; - } else { - var paddArray = modifier.split(placeHolder); - if(paddArray.length == 1){ - if (modifier.indexOf(placeHolder) === 0) { - return val.slice(0, -paddArray[0].length); + } + var filterDebounced = _.debounce(function (e) { + $scope.$apply(function () { + //a canceler exists, so perform the cancelation operation and reset + if (canceler) { + canceler.resolve(); + canceler = $q.defer(); } else { - return val.slice(paddArray[0].length, 0); - } - } else { - if (paddArray[1].length === 0) { - return val.slice(paddArray[0].length); + canceler = $q.defer(); } - return val.slice(paddArray[0].length, -paddArray[1].length); - } + vm.search(); + }); + }, 200); + function filter() { + vm.dashboard.loading = true; + filterDebounced(); } + activate(); + } + angular.module('umbraco').controller('Umbraco.Dashboard.RedirectUrlsController', RedirectUrlsController); + }()); + function XmlDataIntegrityReportController($scope, umbRequestHelper, $log, $http) { + function check(item) { + var action = item.check; + umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('xmlDataIntegrityBaseUrl', action)), 'Failed to retrieve data integrity status').then(function (result) { + item.checking = false; + item.invalid = result === 'false'; + }); } - - var addModifier = function(val, modifier){ - if (!modifier || modifier.indexOf(placeHolder) < 0) { - return val; - } else { - return modifier.replace(placeHolder, val); + $scope.fix = function (item) { + var action = item.fix; + if (item.fix) { + if (confirm('This will cause all xml structures for this type to be rebuilt. ' + 'Depending on how much content there is in your site this could take a while. ' + 'It is not recommended to rebuild xml structures if they are not out of sync, during times of high website traffic ' + 'or when editors are editing content.')) { + item.fixing = true; + umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('xmlDataIntegrityBaseUrl', action)), 'Failed to retrieve data integrity status').then(function (result) { + item.fixing = false; + item.invalid = result === 'false'; + }); + } } }; - - function gridItemHasConfig(styles, config) { - - if(_.isEmpty(styles) && _.isEmpty(config)) { - return false; - } else { - return true; + $scope.items = { + 'contentXml': { + label: 'Content in the cmsContentXml table', + checking: true, + fixing: false, + fix: 'FixContentXmlTable', + check: 'CheckContentXmlTable' + }, + 'mediaXml': { + label: 'Media in the cmsContentXml table', + checking: true, + fixing: false, + fix: 'FixMediaXmlTable', + check: 'CheckMediaXmlTable' + }, + 'memberXml': { + label: 'Members in the cmsContentXml table', + checking: true, + fixing: false, + fix: 'FixMembersXmlTable', + check: 'CheckMembersXmlTable' } - + }; + for (var i in $scope.items) { + check($scope.items[i]); } - - // ********************************************* - // Area management functions - // ********************************************* - - $scope.clickCell = function(index, cells, row) { - cells[index].active = true; - row.hasActiveChild = true; + } + angular.module('umbraco').controller('Umbraco.Dashboard.XmlDataIntegrityReportController', XmlDataIntegrityReportController); + /** + * @ngdoc controller + * @name Umbraco.Editors.DataType.CreateController + * @function + * + * @description + * The controller for the data type creation dialog + */ + function DataTypeCreateController($scope, $location, navigationService, dataTypeResource, formHelper, appState) { + $scope.model = { + folderName: '', + creatingFolder: false }; - - $scope.clickOutsideCell = function(index, cells, row) { - cells[index].active = false; - row.hasActiveChild = hasActiveChild(row, cells); + var node = $scope.dialogOptions.currentNode; + $scope.showCreateFolder = function () { + $scope.model.creatingFolder = true; }; - - $scope.cellPreview = function (cell) { - if (cell && cell.$allowedEditors) { - var editor = cell.$allowedEditors[0]; - return editor.icon; - } else { - return "icon-layout"; + $scope.createContainer = function () { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: this.createFolderForm, + statusMessage: 'Creating folder...' + })) { + dataTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { + navigationService.hideMenu(); + var currPath = node.path ? node.path : '-1'; + navigationService.syncTree({ + tree: 'datatypes', + path: currPath + ',' + folderId, + forceReload: true, + activate: true + }); + formHelper.resetForm({ scope: $scope }); + var section = appState.getSectionState('currentSection'); + }, function (err) { + }); } + ; }; - - - // ********************************************* - // Control management functions - // ********************************************* - $scope.clickControl = function (index, controls, cell) { - controls[index].active = true; - cell.hasActiveChild = true; + $scope.createDataType = function () { + $location.search('create', null); + $location.path('/developer/datatypes/edit/' + node.id).search('create', 'true'); + navigationService.hideMenu(); }; - - $scope.clickOutsideControl = function (index, controls, cell) { - controls[index].active = false; - cell.hasActiveChild = hasActiveChild(cell, controls); + } + angular.module('umbraco').controller('Umbraco.Editors.DataType.CreateController', DataTypeCreateController); + /** + * @ngdoc controller + * @name Umbraco.Editors.ContentDeleteController + * @function + * + * @description + * The controller for deleting content + */ + function DataTypeDeleteController($scope, dataTypeResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + dataTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); }; - - function hasActiveChild(item, children) { - - var activeChild = false; - - for(var i = 0; children.length > i; i++) { - var child = children[i]; - - if(child.active) { - activeChild = true; - } - } - - if(activeChild) { - return true; + $scope.performContainerDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + dataTypeResource.deleteContainerById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.DataType.DeleteController', DataTypeDeleteController); + /** + * @ngdoc controller + * @name Umbraco.Editors.DataType.EditController + * @function + * + * @description + * The controller for the content editor + */ + function DataTypeEditController($scope, $routeParams, $location, appState, navigationService, treeService, dataTypeResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, formHelper, editorState, dataTypeHelper, eventsService) { + //setup scope vars + $scope.page = {}; + $scope.page.loading = false; + $scope.page.nameLocked = false; + $scope.page.menu = {}; + $scope.page.menu.currentSection = appState.getSectionState('currentSection'); + $scope.page.menu.currentNode = null; + var evts = []; + //method used to configure the pre-values when we retrieve them from the server + function createPreValueProps(preVals) { + $scope.preValues = []; + for (var i = 0; i < preVals.length; i++) { + $scope.preValues.push({ + hideLabel: preVals[i].hideLabel, + alias: preVals[i].key, + description: preVals[i].description, + label: preVals[i].label, + view: preVals[i].view, + value: preVals[i].value, + config: preVals[i].config + }); } - } - - - var guid = (function () { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - return function () { - return s4() + s4() + "-" + s4() + "-" + s4() + "-" + - s4() + "-" + s4() + s4() + s4(); - }; - })(); - - $scope.setUniqueId = function (cell, index) { - return guid(); - }; - - $scope.addControl = function (editor, cell, index, initialize) { - - initialize = (initialize !== false); - - var newControl = { - value: null, - editor: editor, - $initializing: initialize - }; - - if (index === undefined) { - index = cell.controls.length; + //set up the standard data type props + $scope.properties = { + selectedEditor: { + alias: 'selectedEditor', + description: 'Select a property editor', + label: 'Property editor' + }, + selectedEditorId: { + alias: 'selectedEditorId', + label: 'Property editor alias' } - - newControl.active = true; - - //populate control - $scope.initControl(newControl, index + 1); - - cell.controls.push(newControl); - - }; - - $scope.addTinyMce = function (cell) { - var rte = $scope.getEditor("rte"); - $scope.addControl(rte, cell); - }; - - $scope.getEditor = function (alias) { - return _.find($scope.availableEditors, function (editor) { return editor.alias === alias; }); - }; - - $scope.removeControl = function (cell, $index) { - $scope.currentControl = null; - cell.controls.splice($index, 1); - }; - - $scope.percentage = function (spans) { - return ((spans / $scope.model.config.items.columns) * 100).toFixed(8); - }; - - - $scope.clearPrompt = function (scopedObject, e) { - scopedObject.deletePrompt = false; - e.preventDefault(); - e.stopPropagation(); - }; - - $scope.togglePrompt = function (scopedObject) { - scopedObject.deletePrompt = !scopedObject.deletePrompt; - }; - - $scope.hidePrompt = function (scopedObject) { - scopedObject.deletePrompt = false; }; - - $scope.toggleAddRow = function() { - $scope.showRowConfigurations = !$scope.showRowConfigurations; - }; - - - // ********************************************* - // Initialization - // these methods are called from ng-init on the template - // so we can controll their first load data - // - // intialization sets non-saved data like percentage sizing, allowed editors and - // other data that should all be pre-fixed with $ to strip it out on save - // ********************************************* - - // ********************************************* - // Init template + sections - // ********************************************* - $scope.initContent = function () { - var clear = true; - - //settings indicator shortcut - if ( ($scope.model.config.items.config && $scope.model.config.items.config.length > 0) || ($scope.model.config.items.styles && $scope.model.config.items.styles.length > 0)) { - $scope.hasSettings = true; + //setup the pre-values as props + $scope.preValues = []; + if ($routeParams.create) { + $scope.page.loading = true; + //we are creating so get an empty data type item + dataTypeResource.getScaffold($routeParams.id).then(function (data) { + $scope.preValuesLoaded = true; + $scope.content = data; + setHeaderNameState($scope.content); + //set a shared state + editorState.set($scope.content); + $scope.page.loading = false; + }); + } else { + loadDataType(); + } + function loadDataType() { + $scope.page.loading = true; + //we are editing so get the content item from the server + dataTypeResource.getById($routeParams.id).then(function (data) { + $scope.preValuesLoaded = true; + $scope.content = data; + createPreValueProps($scope.content.preValues); + setHeaderNameState($scope.content); + //share state + editorState.set($scope.content); + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + navigationService.syncTree({ + tree: 'datatypes', + path: data.path + }).then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }); + $scope.page.loading = false; + }); + } + $scope.$watch('content.selectedEditor', function (newVal, oldVal) { + //when the value changes, we need to dynamically load in the new editor + if (newVal && (newVal != oldVal && (oldVal || $routeParams.create))) { + //we are editing so get the content item from the server + var currDataTypeId = $routeParams.create ? undefined : $routeParams.id; + dataTypeResource.getPreValues(newVal, currDataTypeId).then(function (data) { + $scope.preValuesLoaded = true; + $scope.content.preValues = data; + createPreValueProps($scope.content.preValues); + setHeaderNameState($scope.content); + //share state + editorState.set($scope.content); + }); } - - //ensure the grid has a column value set, - //if nothing is found, set it to 12 - if ($scope.model.config.items.columns && angular.isString($scope.model.config.items.columns)) { - $scope.model.config.items.columns = parseInt($scope.model.config.items.columns); - } else { - $scope.model.config.items.columns = 12; + }); + function setHeaderNameState(content) { + if (content.isSystem == 1) { + $scope.page.nameLocked = true; } - - if ($scope.model.value && $scope.model.value.sections && $scope.model.value.sections.length > 0 && $scope.model.value.sections[0].rows && $scope.model.value.sections[0].rows.length > 0) { - - if ($scope.model.value.name && angular.isArray($scope.model.config.items.templates)) { - - //This will occur if it is an existing value, in which case - // we need to determine which layout was applied by looking up - // the name - // TODO: We need to change this to an immutable ID!! - - var found = _.find($scope.model.config.items.templates, function (t) { - return t.name === $scope.model.value.name; + } + $scope.save = function () { + if (formHelper.submitForm({ + scope: $scope, + statusMessage: 'Saving...' + })) { + $scope.page.saveButtonState = 'busy'; + dataTypeResource.save($scope.content, $scope.preValues, $routeParams.create).then(function (data) { + formHelper.resetForm({ + scope: $scope, + notifications: data.notifications }); - - if (found && angular.isArray(found.sections) && found.sections.length === $scope.model.value.sections.length) { - - //Cool, we've found the template associated with our current value with matching sections counts, now we need to - // merge this template data on to our current value (as if it was new) so that we can preserve what is and isn't - // allowed for this template based on the current config. - - _.each(found.sections, function (templateSection, index) { - angular.extend($scope.model.value.sections[index], angular.copy(templateSection)); - }); - - } - } - - _.forEach($scope.model.value.sections, function (section, index) { - - if (section.grid > 0) { - $scope.initSection(section); - - //we do this to ensure that the grid can be reset by deleting the last row - if (section.rows.length > 0) { - clear = false; + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + rebindCallback: function () { + createPreValueProps(data.preValues); } - } else { - $scope.model.value.sections.splice(index, 1); - } - }); - } else if ($scope.model.config.items.templates && $scope.model.config.items.templates.length === 1) { - $scope.addTemplate($scope.model.config.items.templates[0]); + }); + setHeaderNameState($scope.content); + //share state + editorState.set($scope.content); + navigationService.syncTree({ + tree: 'datatypes', + path: data.path, + forceReload: true + }).then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }); + $scope.page.saveButtonState = 'success'; + dataTypeHelper.rebindChangedProperties($scope.content, data); + }, function (err) { + //NOTE: in the case of data type values we are setting the orig/new props + // to be the same thing since that only really matters for content/media. + contentEditingHelper.handleSaveError({ + redirectOnFailure: false, + err: err + }); + $scope.page.saveButtonState = 'error'; + //share state + editorState.set($scope.content); + }); + } + }; + evts.push(eventsService.on('app.refreshEditor', function (name, error) { + loadDataType(); + })); + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + } + angular.module('umbraco').controller('Umbraco.Editors.DataType.EditController', DataTypeEditController); + angular.module('umbraco').controller('Umbraco.Editors.DataType.MoveController', function ($scope, dataTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; + } + $scope.move = function () { + $scope.busy = true; + $scope.error = false; + dataTypeResource.move({ + parentId: $scope.target.id, + id: dialogOptions.currentNode.id + }).then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + navigationService.syncTree({ + tree: 'dataTypes', + path: path, + forceReload: true, + activate: false + }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ + tree: 'dataTypes', + path: activeNodePath, + forceReload: false, + activate: true + }); + } + }); + eventsService.emit('app.refreshEditor'); + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + }); + }); + angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.CopyController', function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; + } + $scope.copy = function () { + $scope.busy = true; + $scope.error = false; + contentTypeResource.copy({ + parentId: $scope.target.id, + id: dialogOptions.currentNode.id + }).then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + navigationService.syncTree({ + tree: 'documentTypes', + path: path, + forceReload: true, + activate: false + }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ + tree: 'documentTypes', + path: activeNodePath, + forceReload: false, + activate: true + }); + } + }); + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + }); + }); + /** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.CreateController + * @function + * + * @description + * The controller for the doc type creation dialog + */ + function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService) { + $scope.model = { + allowCreateFolder: $scope.dialogOptions.currentNode.parentId === null || $scope.dialogOptions.currentNode.nodeType === 'container', + folderName: '', + creatingFolder: false + }; + var node = $scope.dialogOptions.currentNode, localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); + $scope.showCreateFolder = function () { + $scope.model.creatingFolder = true; + }; + $scope.createContainer = function () { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: this.createFolderForm, + statusMessage: localizeCreateFolder + })) { + contentTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { + navigationService.hideMenu(); + var currPath = node.path ? node.path : '-1'; + navigationService.syncTree({ + tree: 'documenttypes', + path: currPath + ',' + folderId, + forceReload: true, + activate: true + }); + formHelper.resetForm({ scope: $scope }); + var section = appState.getSectionState('currentSection'); + }, function (err) { + $scope.error = err; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + } + }; + $scope.createDocType = function () { + $location.search('create', null); + $location.search('notemplate', null); + $location.path('/settings/documenttypes/edit/' + node.id).search('create', 'true'); + navigationService.hideMenu(); + }; + $scope.createComponent = function () { + $location.search('create', null); + $location.search('notemplate', null); + $location.path('/settings/documenttypes/edit/' + node.id).search('create', 'true').search('notemplate', 'true'); + navigationService.hideMenu(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.CreateController', DocumentTypesCreateController); + /** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.DeleteController + * @function + * + * @description + * The controller for deleting content + */ + function DocumentTypesDeleteController($scope, dataTypeResource, contentTypeResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + contentTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.performContainerDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + contentTypeResource.deleteContainerById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.DeleteController', DocumentTypesDeleteController); + /** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.EditController + * @function + * + * @description + * The controller for the content type editor + */ + (function () { + 'use strict'; + function DocumentTypesEditController($scope, $routeParams, $injector, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { + var vm = this; + var localizeSaving = localizationService.localize('general_saving'); + var evts = []; + vm.save = save; + vm.currentNode = null; + vm.contentType = {}; + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = 'init'; + vm.page.navigation = [ + { + 'name': localizationService.localize('general_design'), + 'icon': 'icon-document-dashed-line', + 'view': 'views/documenttypes/views/design/design.html', + 'active': true + }, + { + 'name': localizationService.localize('general_listView'), + 'icon': 'icon-list', + 'view': 'views/documenttypes/views/listview/listview.html' + }, + { + 'name': localizationService.localize('general_rights'), + 'icon': 'icon-keychain', + 'view': 'views/documenttypes/views/permissions/permissions.html' + }, + { + 'name': localizationService.localize('treeHeaders_templates'), + 'icon': 'icon-layout', + 'view': 'views/documenttypes/views/templates/templates.html' + } + ]; + vm.page.keyboardShortcutsOverview = [ + { + 'name': localizationService.localize('main_sections'), + 'shortcuts': [{ + 'description': localizationService.localize('shortcuts_navigateSections'), + 'keys': [ + { 'key': '1' }, + { 'key': '4' } + ], + 'keyRange': true + }] + }, + { + 'name': localizationService.localize('general_design'), + 'shortcuts': [ + { + 'description': localizationService.localize('shortcuts_addTab'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 't' } + ] + }, + { + 'description': localizationService.localize('shortcuts_addProperty'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'p' } + ] + }, + { + 'description': localizationService.localize('shortcuts_addEditor'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'e' } + ] + }, + { + 'description': localizationService.localize('shortcuts_editDataType'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'd' } + ] + } + ] + }, + { + 'name': localizationService.localize('general_listView'), + 'shortcuts': [{ + 'description': localizationService.localize('shortcuts_toggleListView'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'l' } + ] + }] + }, + { + 'name': localizationService.localize('general_rights'), + 'shortcuts': [ + { + 'description': localizationService.localize('shortcuts_toggleAllowAsRoot'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'r' } + ] + }, + { + 'description': localizationService.localize('shortcuts_addChildNode'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'c' } + ] + } + ] + }, + { + 'name': localizationService.localize('treeHeaders_templates'), + 'shortcuts': [{ + 'description': localizationService.localize('shortcuts_addTemplate'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 't' } + ] + }] + } + ]; + contentTypeHelper.checkModelsBuilderStatus().then(function (result) { + vm.page.modelsBuilder = result; + if (result) { + //Models builder mode: + vm.page.defaultButton = { + hotKey: 'ctrl+s', + hotKeyWhenHidden: true, + labelKey: 'buttons_save', + letter: 'S', + type: 'submit', + handler: function () { + vm.save(); + } + }; + vm.page.subButtons = [{ + hotKey: 'ctrl+g', + hotKeyWhenHidden: true, + labelKey: 'buttons_saveAndGenerateModels', + letter: 'G', + handler: function () { + vm.page.saveButtonState = 'busy'; + vm.save().then(function (result) { + vm.page.saveButtonState = 'busy'; + localizationService.localize('modelsBuilder_buildingModels').then(function (headerValue) { + localizationService.localize('modelsBuilder_waitingMessage').then(function (msgValue) { + notificationsService.info(headerValue, msgValue); + }); + }); + contentTypeHelper.generateModels().then(function (result) { + // generateModels() returns the dashboard content + if (!result.lastError) { + //re-check model status + contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { + vm.page.modelsBuilder = statusResult; + }); + //clear and add success + vm.page.saveButtonState = 'init'; + localizationService.localize('modelsBuilder_modelsGenerated').then(function (value) { + notificationsService.success(value); + }); + } else { + vm.page.saveButtonState = 'error'; + localizationService.localize('modelsBuilder_modelsExceptionInUlog').then(function (value) { + notificationsService.error(value); + }); + } + }, function () { + vm.page.saveButtonState = 'error'; + localizationService.localize('modelsBuilder_modelsGeneratedError').then(function (value) { + notificationsService.error(value); + }); + }); + }); + } + }]; + } + }); + if ($routeParams.create) { + vm.page.loading = true; + //we are creating so get an empty data type item + contentTypeResource.getScaffold($routeParams.id).then(function (dt) { + init(dt); + vm.page.loading = false; + }); + } else { + loadDocumentType(); + } + function loadDocumentType() { + vm.page.loading = true; + contentTypeResource.getById($routeParams.id).then(function (dt) { + init(dt); + syncTreeNode(vm.contentType, dt.path, true); + vm.page.loading = false; + }); + } + /* ---------- SAVE ---------- */ + function save() { + // only save if there is no overlays open + if (overlayHelper.getNumberOfOverlays() === 0) { + var deferred = $q.defer(); + vm.page.saveButtonState = 'busy'; + // reformat allowed content types to array if id's + vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: contentTypeResource.save, + scope: $scope, + content: vm.contentType, + //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc + // type when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + // we need to rebind... the IDs that have been created! + rebindCallback: function (origContentType, savedContentType) { + vm.contentType.id = savedContentType.id; + vm.contentType.groups.forEach(function (group) { + if (!group.name) + return; + var k = 0; + while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) + k++; + if (k == savedContentType.groups.length) { + group.id = 0; + return; + } + var savedGroup = savedContentType.groups[k]; + if (!group.id) + group.id = savedGroup.id; + group.properties.forEach(function (property) { + if (property.id || !property.alias) + return; + k = 0; + while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) + k++; + if (k == savedGroup.properties.length) { + property.id = 0; + return; + } + var savedProperty = savedGroup.properties[k]; + property.id = savedProperty.id; + }); + }); + } + }).then(function (data) { + //success + syncTreeNode(vm.contentType, data.path); + vm.page.saveButtonState = 'success'; + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } else { + localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { + localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } + vm.page.saveButtonState = 'error'; + deferred.reject(err); + }); + return deferred.promise; + } + } + function init(contentType) { + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); + }); + } + // insert template on new doc types + if (!$routeParams.notemplate && contentType.id === 0) { + contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); + contentType.allowedTemplates = contentTypeHelper.insertTemplatePlaceholder(contentType.allowedTemplates); + } + // convert icons for content type + convertLegacyIcons(contentType); + //set a shared state + editorState.set(contentType); + vm.contentType = contentType; + } + function convertLegacyIcons(contentType) { + // make array to store contentType icon + var contentTypeArray = []; + // push icon to array + contentTypeArray.push({ 'icon': contentType.icon }); + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + } + function getDataTypeDetails(property) { + if (property.propertyState !== 'init') { + dataTypeResource.getById(property.dataTypeId).then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + } + } + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { + navigationService.syncTree({ + tree: 'documenttypes', + path: path.split(','), + forceReload: initialLoad !== true + }).then(function (syncArgs) { + vm.currentNode = syncArgs.node; + }); + } + evts.push(eventsService.on('app.refreshEditor', function (name, error) { + loadDocumentType(); + })); + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + } + angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.EditController', DocumentTypesEditController); + }()); + angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.MoveController', function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; + } + $scope.move = function () { + $scope.busy = true; + $scope.error = false; + contentTypeResource.move({ + parentId: $scope.target.id, + id: dialogOptions.currentNode.id + }).then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + navigationService.syncTree({ + tree: 'documentTypes', + path: path, + forceReload: true, + activate: false + }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ + tree: 'documentTypes', + path: activeNodePath, + forceReload: false, + activate: true + }); + } + }); + eventsService.emit('app.refreshEditor'); + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + }); + }); + angular.module('umbraco').controller('Umbraco.Editors.ContentTypeContainers.RenameController', [ + '$scope', + '$injector', + 'navigationService', + 'notificationsService', + 'localizationService', + function (scope, injector, navigationService, notificationsService, localizationService) { + var notificationHeader; + function reportSuccessAndClose(treeName) { + var lastComma = scope.currentNode.path.lastIndexOf(','), path = lastComma === -1 ? scope.currentNode.path : scope.currentNode.path.substring(0, lastComma - 1); + navigationService.syncTree({ + tree: treeName, + path: path, + forceReload: true, + activate: true + }); + localizationService.localize('renamecontainer_folderWasRenamed', [ + scope.currentNode.name, + scope.model.folderName + ]).then(function (msg) { + notificationsService.showNotification({ + type: 0, + header: notificationHeader, + message: msg + }); + }); + navigationService.hideMenu(); + } + localizationService.localize('renamecontainer_renamed').then(function (s) { + notificationHeader = s; + }); + scope.model = { folderName: scope.currentNode.name }; + scope.renameContainer = function (resourceKey, treeName) { + var resource = injector.get(resourceKey); + resource.renameContainer(scope.currentNode.id, scope.model.folderName).then(function () { + reportSuccessAndClose(treeName); + }, function (err) { + scope.error = err; + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + } + ]); + /** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ + (function () { + 'use strict'; + function PermissionsController($scope, contentTypeResource, iconHelper, contentTypeHelper, localizationService) { + /* ----------- SCOPE VARIABLES ----------- */ + var vm = this; + var childNodeSelectorOverlayTitle = ''; + vm.contentTypes = []; + vm.selectedChildren = []; + vm.overlayTitle = ''; + vm.addChild = addChild; + vm.removeChild = removeChild; + /* ---------- INIT ---------- */ + init(); + function init() { + childNodeSelectorOverlayTitle = localizationService.localize('contentTypeEditor_chooseChildNode'); + contentTypeResource.getAll().then(function (contentTypes) { + vm.contentTypes = contentTypes; + // convert legacy icons + iconHelper.formatContentTypeIcons(vm.contentTypes); + vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.contentTypes); + if ($scope.model.id === 0) { + contentTypeHelper.insertChildNodePlaceholder(vm.contentTypes, $scope.model.name, $scope.model.icon, $scope.model.id); + } + }); + } + function addChild($event) { + vm.childNodeSelectorOverlay = { + view: 'itempicker', + title: childNodeSelectorOverlayTitle, + availableItems: vm.contentTypes, + selectedItems: vm.selectedChildren, + event: $event, + show: true, + submit: function (model) { + vm.selectedChildren.push(model.selectedItem); + $scope.model.allowedContentTypes.push(model.selectedItem.id); + vm.childNodeSelectorOverlay.show = false; + vm.childNodeSelectorOverlay = null; + } + }; + } + function removeChild(selectedChild, index) { + // remove from vm + vm.selectedChildren.splice(index, 1); + // remove from content type model + var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); + $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); + } + } + angular.module('umbraco').controller('Umbraco.Editors.DocumentType.PermissionsController', PermissionsController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.TemplatesController + * @function + * + * @description + * The controller for the content type editor templates sub view + */ + (function () { + 'use strict'; + function TemplatesController($scope, entityResource, contentTypeHelper, $routeParams) { + /* ----------- SCOPE VARIABLES ----------- */ + var vm = this; + vm.availableTemplates = []; + vm.updateTemplatePlaceholder = false; + /* ---------- INIT ---------- */ + init(); + function init() { + entityResource.getAll('Template').then(function (templates) { + vm.availableTemplates = templates; + // update placeholder template information on new doc types + if (!$routeParams.notemplate && $scope.model.id === 0) { + vm.updateTemplatePlaceholder = true; + vm.availableTemplates = contentTypeHelper.insertTemplatePlaceholder(vm.availableTemplates); + } + }); + } + } + angular.module('umbraco').controller('Umbraco.Editors.DocumentType.TemplatesController', TemplatesController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.Media.CreateController + * @function + * + * @description + * The controller for the media creation dialog + */ + function mediaCreateController($scope, $routeParams, mediaTypeResource, iconHelper) { + mediaTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { + $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); + }); + } + angular.module('umbraco').controller('Umbraco.Editors.Media.CreateController', mediaCreateController); + /** + * @ngdoc controller + * @name Umbraco.Editors.ContentDeleteController + * @function + * + * @description + * The controller for deleting content + */ + function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, dialogService, notificationsService) { + $scope.performDelete = function () { + // stop from firing again on double-click + if ($scope.busy) { + return false; + } + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + $scope.busy = true; + mediaResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + treeService.removeNode($scope.currentNode); + if (rootNode) { + //ensure the recycle bin has child nodes now + var recycleBin = treeService.getDescendantNode(rootNode, -21); + if (recycleBin) { + recycleBin.hasChildren = true; + } + } + //if the current edited item is the same one as we're deleting, we need to navigate elsewhere + if (editorState.current && editorState.current.id == $scope.currentNode.id) { + //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent + var location = '/media'; + if ($scope.currentNode.parentId.toString() !== '-1') + location = '/media/media/edit/' + $scope.currentNode.parentId; + $location.path(location); + } + navigationService.hideMenu(); + }, function (err) { + $scope.currentNode.loading = false; + $scope.busy = false; + //check if response is ysod + if (err.status && err.status >= 500) { + dialogService.ysodDialog(err); + } + if (err.data && angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Media.DeleteController', MediaDeleteController); + /** + * @ngdoc controller + * @name Umbraco.Editors.Media.EditController + * @function + * + * @description + * The controller for the media editor + */ + function mediaEditController($scope, $routeParams, appState, mediaResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, treeService, formHelper, umbModelMapper, editorState, umbRequestHelper, $http) { + //setup scope vars + $scope.currentSection = appState.getSectionState('currentSection'); + $scope.currentNode = null; + //the editors affiliated node + $scope.page = {}; + $scope.page.loading = false; + $scope.page.menu = {}; + $scope.page.menu.currentSection = appState.getSectionState('currentSection'); + $scope.page.menu.currentNode = null; + //the editors affiliated node + $scope.page.listViewPath = null; + $scope.page.saveButtonState = 'init'; + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(content, path, initialLoad) { + if (!$scope.content.isChildOfListView) { + navigationService.syncTree({ + tree: 'media', + path: path.split(','), + forceReload: initialLoad !== true + }).then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }); + } else if (initialLoad === true) { + //it's a child item, just sync the ui node to the parent + navigationService.syncTree({ + tree: 'media', + path: path.substring(0, path.lastIndexOf(',')).split(','), + forceReload: initialLoad !== true + }); + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + // from the server so that we can load in the actions menu. + umbRequestHelper.resourcePromise($http.get(content.treeNodeUrl), 'Failed to retrieve data for child node ' + content.id).then(function (node) { + $scope.page.menu.currentNode = node; + }); + } + } + if ($routeParams.create) { + $scope.page.loading = true; + mediaResource.getScaffold($routeParams.id, $routeParams.doctype).then(function (data) { + $scope.content = data; + editorState.set($scope.content); + $scope.page.loading = false; + }); + } else { + $scope.page.loading = true; + mediaResource.getById($routeParams.id).then(function (data) { + $scope.content = data; + if (data.isChildOfListView && data.trashed === false) { + $scope.page.listViewPath = $routeParams.page ? '/media/media/edit/' + data.parentId + '?page=' + $routeParams.page : '/media/media/edit/' + data.parentId; + } + editorState.set($scope.content); + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + syncTreeNode($scope.content, data.path, true); + if ($scope.content.parentId && $scope.content.parentId != -1) { + //We fetch all ancestors of the node to generate the footer breadcrump navigation + entityResource.getAncestors($routeParams.id, 'media').then(function (anc) { + $scope.ancestors = anc; + }); + } + $scope.page.loading = false; + }); + } + $scope.save = function () { + if (!$scope.busy && formHelper.submitForm({ + scope: $scope, + statusMessage: 'Saving...' + })) { + $scope.busy = true; + $scope.page.saveButtonState = 'busy'; + mediaResource.save($scope.content, $routeParams.create, fileManager.getFiles()).then(function (data) { + formHelper.resetForm({ + scope: $scope, + notifications: data.notifications + }); + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + editorState.set($scope.content); + $scope.busy = false; + syncTreeNode($scope.content, data.path); + $scope.page.saveButtonState = 'success'; + }, function (err) { + contentEditingHelper.handleSaveError({ + err: err, + redirectOnFailure: true, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) + }); + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + editorState.set($scope.content); + $scope.busy = false; + $scope.page.saveButtonState = 'error'; + }); + } else { + $scope.busy = false; + } + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Media.EditController', mediaEditController); + /** + * @ngdoc controller + * @name Umbraco.Editors.Media.EmptyRecycleBinController + * @function + * + * @description + * The controller for deleting media + */ + function MediaEmptyRecycleBinController($scope, mediaResource, treeService, navigationService, notificationsService, $route) { + $scope.busy = false; + $scope.performDelete = function () { + //(used in the UI) + $scope.busy = true; + $scope.currentNode.loading = true; + mediaResource.emptyRecycleBin($scope.currentNode.id).then(function (result) { + $scope.busy = false; + $scope.currentNode.loading = false; + //show any notifications + if (angular.isArray(result.notifications)) { + for (var i = 0; i < result.notifications.length; i++) { + notificationsService.showNotification(result.notifications[i]); + } + } + treeService.removeChildNodes($scope.currentNode); + navigationService.hideMenu(); + //reload the current view + $route.reload(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Media.EmptyRecycleBinController', MediaEmptyRecycleBinController); + //used for the media picker dialog + angular.module('umbraco').controller('Umbraco.Editors.Media.MoveController', function ($scope, userService, eventsService, mediaResource, appState, treeService, navigationService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + var node = dialogOptions.currentNode; + $scope.treeModel = { hideHeader: false }; + userService.getCurrentUser().then(function (userData) { + $scope.treeModel.hideHeader = userData.startMediaIds.length > 0 && userData.startMediaIds.indexOf(-1) == -1; + }); + function nodeSelectHandler(ev, args) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); + } + eventsService.emit('editors.media.moveController.select', args); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; + } + function nodeExpandedHandler(ev, args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); + $scope.move = function () { + mediaResource.move({ + parentId: $scope.target.id, + id: node.id + }).then(function (path) { + $scope.error = false; + $scope.success = true; + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + navigationService.syncTree({ + tree: 'media', + path: path, + forceReload: true, + activate: false + }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ + tree: 'media', + path: activeNodePath, + forceReload: false, + activate: true + }); + } + }); + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + }; + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); + }); + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + function openMiniListView(node) { + $scope.miniListView = node; + } + }); + /** + * @ngdoc controller + * @name Umbraco.Editors.Content.MediaRecycleBinController + * @function + * + * @description + * Controls the recycle bin for media + * + */ + function MediaRecycleBinController($scope, $routeParams, mediaResource, navigationService, localizationService) { + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.page = {}; + $scope.page.name = 'Recycle Bin'; + $scope.page.nameLocked = true; + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.listViewPath = null; + $routeParams.id = '-21'; + mediaResource.getRecycleBin().then(function (result) { + //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a + // single property, so we'll extract that property (list view) and use it's data. + var listproperty = result.tabs[0].properties[0]; + _.each(listproperty.config, function (val, key) { + $scope.model.config[key] = val; + }); + $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; + }); + $scope.model = { + config: { + entityType: $routeParams.section, + layouts: [] + } + }; + // sync tree node + navigationService.syncTree({ + tree: 'media', + path: [ + '-1', + $routeParams.id + ], + forceReload: false + }); + localizePageName(); + function localizePageName() { + var pageName = 'general_recycleBin'; + localizationService.localize(pageName).then(function (value) { + $scope.page.name = value; + }); + } + } + angular.module('umbraco').controller('Umbraco.Editors.Media.RecycleBinController', MediaRecycleBinController); + angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.CopyController', function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; + } + $scope.copy = function () { + $scope.busy = true; + $scope.error = false; + mediaTypeResource.copy({ + parentId: $scope.target.id, + id: dialogOptions.currentNode.id + }).then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + navigationService.syncTree({ + tree: 'mediaTypes', + path: path, + forceReload: true, + activate: false + }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ + tree: 'mediaTypes', + path: activeNodePath, + forceReload: false, + activate: true + }); + } + }); + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + }); + }); + /** + * @ngdoc controller + * @name Umbraco.Editors.MediaType.CreateController + * @function + * + * @description + * The controller for the media type creation dialog + */ + function MediaTypesCreateController($scope, $location, navigationService, mediaTypeResource, formHelper, appState, localizationService) { + $scope.model = { + folderName: '', + creatingFolder: false + }; + var node = $scope.dialogOptions.currentNode, localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); + $scope.showCreateFolder = function () { + $scope.model.creatingFolder = true; + }; + $scope.createContainer = function () { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: this.createFolderForm, + statusMessage: localizeCreateFolder + })) { + mediaTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { + navigationService.hideMenu(); + var currPath = node.path ? node.path : '-1'; + navigationService.syncTree({ + tree: 'mediatypes', + path: currPath + ',' + folderId, + forceReload: true, + activate: true + }); + formHelper.resetForm({ scope: $scope }); + var section = appState.getSectionState('currentSection'); + }, function (err) { + }); + } + ; + }; + $scope.createMediaType = function () { + $location.search('create', null); + $location.path('/settings/mediatypes/edit/' + node.id).search('create', 'true'); + navigationService.hideMenu(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.CreateController', MediaTypesCreateController); + /** + * @ngdoc controller + * @name Umbraco.Editors.MediaType.DeleteController + * @function + * + * @description + * The controller for the media type delete dialog + */ + function MediaTypesDeleteController($scope, dataTypeResource, mediaTypeResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + mediaTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.performContainerDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + mediaTypeResource.deleteContainerById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.DeleteController', MediaTypesDeleteController); + /** + * @ngdoc controller + * @name Umbraco.Editors.MediaType.EditController + * @function + * + * @description + * The controller for the media type editor + */ + (function () { + 'use strict'; + function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { + var vm = this; + var localizeSaving = localizationService.localize('general_saving'); + var evts = []; + vm.save = save; + vm.currentNode = null; + vm.contentType = {}; + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = 'init'; + vm.page.navigation = [ + { + 'name': localizationService.localize('general_design'), + 'icon': 'icon-document-dashed-line', + 'view': 'views/mediatypes/views/design/design.html', + 'active': true + }, + { + 'name': localizationService.localize('general_listView'), + 'icon': 'icon-list', + 'view': 'views/mediatypes/views/listview/listview.html' + }, + { + 'name': localizationService.localize('general_rights'), + 'icon': 'icon-keychain', + 'view': 'views/mediatypes/views/permissions/permissions.html' + } + ]; + vm.page.keyboardShortcutsOverview = [ + { + 'name': localizationService.localize('main_sections'), + 'shortcuts': [{ + 'description': localizationService.localize('shortcuts_navigateSections'), + 'keys': [ + { 'key': '1' }, + { 'key': '3' } + ], + 'keyRange': true + }] + }, + { + 'name': localizationService.localize('general_design'), + 'shortcuts': [ + { + 'description': localizationService.localize('shortcuts_addTab'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 't' } + ] + }, + { + 'description': localizationService.localize('shortcuts_addProperty'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'p' } + ] + }, + { + 'description': localizationService.localize('shortcuts_addEditor'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'e' } + ] + }, + { + 'description': localizationService.localize('shortcuts_editDataType'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'd' } + ] + } + ] + }, + { + 'name': localizationService.localize('general_listView'), + 'shortcuts': [{ + 'description': localizationService.localize('shortcuts_toggleListView'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'l' } + ] + }] + }, + { + 'name': localizationService.localize('general_rights'), + 'shortcuts': [ + { + 'description': localizationService.localize('shortcuts_toggleAllowAsRoot'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'r' } + ] + }, + { + 'description': localizationService.localize('shortcuts_addChildNode'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'c' } + ] + } + ] + } + ]; + contentTypeHelper.checkModelsBuilderStatus().then(function (result) { + vm.page.modelsBuilder = result; + if (result) { + //Models builder mode: + vm.page.defaultButton = { + hotKey: 'ctrl+s', + hotKeyWhenHidden: true, + labelKey: 'buttons_save', + letter: 'S', + type: 'submit', + handler: function () { + vm.save(); + } + }; + vm.page.subButtons = [{ + hotKey: 'ctrl+g', + hotKeyWhenHidden: true, + labelKey: 'buttons_saveAndGenerateModels', + letter: 'G', + handler: function () { + vm.page.saveButtonState = 'busy'; + vm.save().then(function (result) { + vm.page.saveButtonState = 'busy'; + localizationService.localize('modelsBuilder_buildingModels').then(function (headerValue) { + localizationService.localize('modelsBuilder_waitingMessage').then(function (msgValue) { + notificationsService.info(headerValue, msgValue); + }); + }); + contentTypeHelper.generateModels().then(function (result) { + if (result.success) { + //re-check model status + contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { + vm.page.modelsBuilder = statusResult; + }); + //clear and add success + vm.page.saveButtonState = 'init'; + localizationService.localize('modelsBuilder_modelsGenerated').then(function (value) { + notificationsService.success(value); + }); + } else { + vm.page.saveButtonState = 'error'; + localizationService.localize('modelsBuilder_modelsExceptionInUlog').then(function (value) { + notificationsService.error(value); + }); + } + }, function () { + vm.page.saveButtonState = 'error'; + localizationService.localize('modelsBuilder_modelsGeneratedError').then(function (value) { + notificationsService.error(value); + }); + }); + }); + } + }]; + } + }); + if ($routeParams.create) { + vm.page.loading = true; + //we are creating so get an empty data type item + mediaTypeResource.getScaffold($routeParams.id).then(function (dt) { + init(dt); + vm.page.loading = false; + }); + } else { + loadMediaType(); + } + function loadMediaType() { + vm.page.loading = true; + mediaTypeResource.getById($routeParams.id).then(function (dt) { + init(dt); + syncTreeNode(vm.contentType, dt.path, true); + vm.page.loading = false; + }); + } + /* ---------- SAVE ---------- */ + function save() { + // only save if there is no overlays open + if (overlayHelper.getNumberOfOverlays() === 0) { + var deferred = $q.defer(); + vm.page.saveButtonState = 'busy'; + // reformat allowed content types to array if id's + vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: mediaTypeResource.save, + scope: $scope, + content: vm.contentType, + //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc + // type when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + // we need to rebind... the IDs that have been created! + rebindCallback: function (origContentType, savedContentType) { + vm.contentType.id = savedContentType.id; + vm.contentType.groups.forEach(function (group) { + if (!group.name) + return; + var k = 0; + while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) + k++; + if (k == savedContentType.groups.length) { + group.id = 0; + return; + } + var savedGroup = savedContentType.groups[k]; + if (!group.id) + group.id = savedGroup.id; + group.properties.forEach(function (property) { + if (property.id || !property.alias) + return; + k = 0; + while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) + k++; + if (k == savedGroup.properties.length) { + property.id = 0; + return; + } + var savedProperty = savedGroup.properties[k]; + property.id = savedProperty.id; + }); + }); + } + }).then(function (data) { + //success + syncTreeNode(vm.contentType, data.path); + vm.page.saveButtonState = 'success'; + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } else { + localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { + localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } + vm.page.saveButtonState = 'error'; + deferred.reject(err); + }); + return deferred.promise; + } + } + function init(contentType) { + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); + }); + } + // convert icons for content type + convertLegacyIcons(contentType); + //set a shared state + editorState.set(contentType); + vm.contentType = contentType; + } + function convertLegacyIcons(contentType) { + // make array to store contentType icon + var contentTypeArray = []; + // push icon to array + contentTypeArray.push({ 'icon': contentType.icon }); + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + } + function getDataTypeDetails(property) { + if (property.propertyState !== 'init') { + dataTypeResource.getById(property.dataTypeId).then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + } + } + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { + navigationService.syncTree({ + tree: 'mediatypes', + path: path.split(','), + forceReload: initialLoad !== true + }).then(function (syncArgs) { + vm.currentNode = syncArgs.node; + }); + } + evts.push(eventsService.on('app.refreshEditor', function (name, error) { + loadMediaType(); + })); + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + } + angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.EditController', MediaTypesEditController); + }()); + angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.MoveController', function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; + } + $scope.move = function () { + $scope.busy = true; + $scope.error = false; + mediaTypeResource.move({ + parentId: $scope.target.id, + id: dialogOptions.currentNode.id + }).then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + navigationService.syncTree({ + tree: 'mediaTypes', + path: path, + forceReload: true, + activate: false + }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ + tree: 'mediaTypes', + path: activeNodePath, + forceReload: false, + activate: true + }); + } + }); + eventsService.emit('app.refreshEditor'); + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); + }); + }); + (function () { + 'use strict'; + function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService) { + /* ----------- SCOPE VARIABLES ----------- */ + var vm = this; + var childNodeSelectorOverlayTitle = ''; + vm.mediaTypes = []; + vm.selectedChildren = []; + vm.addChild = addChild; + vm.removeChild = removeChild; + /* ---------- INIT ---------- */ + init(); + function init() { + childNodeSelectorOverlayTitle = localizationService.localize('contentTypeEditor_chooseChildNode'); + mediaTypeResource.getAll().then(function (mediaTypes) { + vm.mediaTypes = mediaTypes; + // convert legacy icons + iconHelper.formatContentTypeIcons(vm.mediaTypes); + vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.mediaTypes); + if ($scope.model.id === 0) { + contentTypeHelper.insertChildNodePlaceholder(vm.mediaTypes, $scope.model.name, $scope.model.icon, $scope.model.id); + } + }); + } + function addChild($event) { + vm.childNodeSelectorOverlay = { + view: 'itempicker', + title: childNodeSelectorOverlayTitle, + availableItems: vm.mediaTypes, + selectedItems: vm.selectedChildren, + event: $event, + show: true, + submit: function (model) { + vm.selectedChildren.push(model.selectedItem); + $scope.model.allowedContentTypes.push(model.selectedItem.id); + vm.childNodeSelectorOverlay.show = false; + vm.childNodeSelectorOverlay = null; + } + }; + } + function removeChild(selectedChild, index) { + // remove from vm + vm.selectedChildren.splice(index, 1); + // remove from content type model + var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); + $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); + } + } + angular.module('umbraco').controller('Umbraco.Editors.MediaType.PermissionsController', PermissionsController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.Member.CreateController + * @function + * + * @description + * The controller for the member creation dialog + */ + function memberCreateController($scope, $routeParams, memberTypeResource, iconHelper) { + memberTypeResource.getTypes($scope.currentNode.id).then(function (data) { + $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); + }); + } + angular.module('umbraco').controller('Umbraco.Editors.Member.CreateController', memberCreateController); + /** + * @ngdoc controller + * @name Umbraco.Editors.Member.DeleteController + * @function + * + * @description + * The controller for deleting content + */ + function MemberDeleteController($scope, memberResource, treeService, navigationService, editorState, $location, $routeParams) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + memberResource.deleteByKey($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + treeService.removeNode($scope.currentNode); + //if the current edited item is the same one as we're deleting, we need to navigate elsewhere + if (editorState.current && editorState.current.key == $scope.currentNode.id) { + $location.path('/member/member/list/' + ($routeParams.listName ? $routeParams.listName : 'all-members')); + } + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Member.DeleteController', MemberDeleteController); + /** + * @ngdoc controller + * @name Umbraco.Editors.Member.EditController + * @function + * + * @description + * The controller for the member editor + */ + function MemberEditController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper, editorState, umbRequestHelper, $http) { + //setup scope vars + $scope.page = {}; + $scope.page.loading = true; + $scope.page.menu = {}; + $scope.page.menu.currentSection = appState.getSectionState('currentSection'); + $scope.page.menu.currentNode = null; + //the editors affiliated node + $scope.page.nameLocked = false; + $scope.page.listViewPath = null; + $scope.page.saveButtonState = 'init'; + $scope.busy = false; + $scope.page.listViewPath = $routeParams.page && $routeParams.listName ? '/member/member/list/' + $routeParams.listName + '?page=' + $routeParams.page : null; + //build a path to sync the tree with + function buildTreePath(data) { + return $routeParams.listName ? '-1,' + $routeParams.listName : '-1'; + } + if ($routeParams.create) { + //if there is no doc type specified then we are going to assume that + // we are not using the umbraco membership provider + if ($routeParams.doctype) { + //we are creating so get an empty member item + memberResource.getScaffold($routeParams.doctype).then(function (data) { + $scope.content = data; + setHeaderNameState($scope.content); + editorState.set($scope.content); + $scope.page.loading = false; + }); + } else { + memberResource.getScaffold().then(function (data) { + $scope.content = data; + setHeaderNameState($scope.content); + editorState.set($scope.content); + $scope.page.loading = false; + }); + } + } else { + //so, we usually refernce all editors with the Int ID, but with members we have + //a different pattern, adding a route-redirect here to handle this: + //isNumber doesnt work here since its seen as a string + //TODO: Why is this here - I don't understand why this would ever be an integer? This will not work when we support non-umbraco membership providers. + if ($routeParams.id && $routeParams.id.length < 9) { + entityResource.getById($routeParams.id, 'Member').then(function (entity) { + $location.path('/member/member/edit/' + entity.key); + }); + } else { + //we are editing so get the content item from the server + memberResource.getByKey($routeParams.id).then(function (data) { + $scope.content = data; + setHeaderNameState($scope.content); + editorState.set($scope.content); + var path = buildTreePath(data); + //sync the tree (only for ui purposes) + navigationService.syncTree({ + tree: 'member', + path: path.split(',') + }); + //it's the initial load of the editor, we need to get the tree node + // from the server so that we can load in the actions menu. + umbRequestHelper.resourcePromise($http.get(data.treeNodeUrl), 'Failed to retrieve data for child node ' + data.key).then(function (node) { + $scope.page.menu.currentNode = node; + }); + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + $scope.page.loading = false; + }); + } + } + function setHeaderNameState(content) { + if (content.membershipScenario === 0) { + $scope.page.nameLocked = true; + } + } + $scope.save = function () { + if (!$scope.busy && formHelper.submitForm({ + scope: $scope, + statusMessage: 'Saving...' + })) { + $scope.busy = true; + $scope.page.saveButtonState = 'busy'; + memberResource.save($scope.content, $routeParams.create, fileManager.getFiles()).then(function (data) { + formHelper.resetForm({ + scope: $scope, + notifications: data.notifications + }); + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + //specify a custom id to redirect to since we want to use the GUID + redirectId: data.key, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + editorState.set($scope.content); + $scope.busy = false; + $scope.page.saveButtonState = 'success'; + var path = buildTreePath(data); + //sync the tree (only for ui purposes) + navigationService.syncTree({ + tree: 'member', + path: path.split(','), + forceReload: true + }); + }, function (err) { + contentEditingHelper.handleSaveError({ + redirectOnFailure: false, + err: err, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) + }); + editorState.set($scope.content); + $scope.busy = false; + $scope.page.saveButtonState = 'error'; + }); + } else { + $scope.busy = false; + } + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Member.EditController', MemberEditController); + /** + * @ngdoc controller + * @name Umbraco.Editors.Member.ListController + * @function + * + * @description + * The controller for the member list view + */ + function MemberListController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper, editorState, localizationService) { + //setup scope vars + $scope.currentSection = appState.getSectionState('currentSection'); + $scope.currentNode = null; + //the editors affiliated node + $scope.page = {}; + $scope.page.lockedName = true; + $scope.page.loading = true; + //we are editing so get the content item from the server + memberResource.getListNode($routeParams.id).then(function (data) { + $scope.content = data; + //translate "All Members" + if ($scope.content != null && $scope.content.name != null && $scope.content.name.replace(' ', '').toLowerCase() == 'allmembers') { + localizationService.localize('member_allMembers').then(function (value) { + $scope.content.name = value; + }); + } + editorState.set($scope.content); + navigationService.syncTree({ + tree: 'member', + path: data.path.split(',') + }).then(function (syncArgs) { + $scope.currentNode = syncArgs.node; + }); + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + $scope.page.loading = false; + }); + } + angular.module('umbraco').controller('Umbraco.Editors.Member.ListController', MemberListController); + /** + * @ngdoc controller + * @name Umbraco.Editors.MemberType.CreateController + * @function + * + * @description + * The controller for the member type creation dialog + */ + function MemberTypesCreateController($scope, $location, navigationService, memberTypeResource, formHelper, appState, localizationService) { + $scope.model = { + folderName: '', + creatingFolder: false + }; + var node = $scope.dialogOptions.currentNode, localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); + $scope.showCreateFolder = function () { + $scope.model.creatingFolder = true; + }; + $scope.createContainer = function () { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: this.createFolderForm, + statusMessage: localizeCreateFolder + })) { + memberTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { + navigationService.hideMenu(); + var currPath = node.path ? node.path : '-1'; + navigationService.syncTree({ + tree: 'membertypes', + path: currPath + ',' + folderId, + forceReload: true, + activate: true + }); + formHelper.resetForm({ scope: $scope }); + var section = appState.getSectionState('currentSection'); + }, function (err) { + }); + } + ; + }; + $scope.createMemberType = function () { + $location.search('create', null); + $location.path('/settings/membertypes/edit/' + node.id).search('create', 'true'); + navigationService.hideMenu(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.MemberTypes.CreateController', MemberTypesCreateController); + /** + * @ngdoc controller + * @name Umbraco.Editors.MemberTypes.DeleteController + * @function + * + * @description + * The controller for deleting member types + */ + function MemberTypesDeleteController($scope, memberTypeResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + memberTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.MemberTypes.DeleteController', MemberTypesDeleteController); + /** + * @ngdoc controller + * @name Umbraco.Editors.MemberType.EditController + * @function + * + * @description + * The controller for the member type editor + */ + (function () { + 'use strict'; + function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q, localizationService, overlayHelper, contentTypeHelper) { + var vm = this; + var localizeSaving = localizationService.localize('general_saving'); + vm.save = save; + vm.currentNode = null; + vm.contentType = {}; + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = 'init'; + vm.page.navigation = [{ + 'name': localizationService.localize('general_design'), + 'icon': 'icon-document-dashed-line', + 'view': 'views/membertypes/views/design/design.html', + 'active': true + }]; + vm.page.keyboardShortcutsOverview = [{ + 'name': localizationService.localize('shortcuts_shortcut'), + 'shortcuts': [ + { + 'description': localizationService.localize('shortcuts_addTab'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 't' } + ] + }, + { + 'description': localizationService.localize('shortcuts_addProperty'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'p' } + ] + }, + { + 'description': localizationService.localize('shortcuts_addEditor'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'e' } + ] + }, + { + 'description': localizationService.localize('shortcuts_editDataType'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'd' } + ] + } + ] + }]; + contentTypeHelper.checkModelsBuilderStatus().then(function (result) { + vm.page.modelsBuilder = result; + if (result) { + //Models builder mode: + vm.page.defaultButton = { + hotKey: 'ctrl+s', + hotKeyWhenHidden: true, + labelKey: 'buttons_save', + letter: 'S', + type: 'submit', + handler: function () { + vm.save(); + } + }; + vm.page.subButtons = [{ + hotKey: 'ctrl+g', + hotKeyWhenHidden: true, + labelKey: 'buttons_saveAndGenerateModels', + letter: 'G', + handler: function () { + vm.page.saveButtonState = 'busy'; + vm.save().then(function (result) { + vm.page.saveButtonState = 'busy'; + localizationService.localize('modelsBuilder_buildingModels').then(function (headerValue) { + localizationService.localize('modelsBuilder_waitingMessage').then(function (msgValue) { + notificationsService.info(headerValue, msgValue); + }); + }); + contentTypeHelper.generateModels().then(function (result) { + if (result.success) { + //re-check model status + contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { + vm.page.modelsBuilder = statusResult; + }); + //clear and add success + vm.page.saveButtonState = 'init'; + localizationService.localize('modelsBuilder_modelsGenerated').then(function (value) { + notificationsService.success(value); + }); + } else { + vm.page.saveButtonState = 'error'; + localizationService.localize('modelsBuilder_modelsExceptionInUlog').then(function (value) { + notificationsService.error(value); + }); + } + }, function () { + vm.page.saveButtonState = 'error'; + localizationService.localize('modelsBuilder_modelsGeneratedError').then(function (value) { + notificationsService.error(value); + }); + }); + }); + } + }]; + } + }); + if ($routeParams.create) { + vm.page.loading = true; + //we are creating so get an empty data type item + memberTypeResource.getScaffold($routeParams.id).then(function (dt) { + init(dt); + vm.page.loading = false; + }); + } else { + vm.page.loading = true; + memberTypeResource.getById($routeParams.id).then(function (dt) { + init(dt); + syncTreeNode(vm.contentType, dt.path, true); + vm.page.loading = false; + }); + } + function save() { + // only save if there is no overlays open + if (overlayHelper.getNumberOfOverlays() === 0) { + var deferred = $q.defer(); + vm.page.saveButtonState = 'busy'; + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: memberTypeResource.save, + scope: $scope, + content: vm.contentType, + //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc + // type when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + // we need to rebind... the IDs that have been created! + rebindCallback: function (origContentType, savedContentType) { + vm.contentType.id = savedContentType.id; + vm.contentType.groups.forEach(function (group) { + if (!group.name) + return; + var k = 0; + while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) + k++; + if (k == savedContentType.groups.length) { + group.id = 0; + return; + } + var savedGroup = savedContentType.groups[k]; + if (!group.id) + group.id = savedGroup.id; + group.properties.forEach(function (property) { + if (property.id || !property.alias) + return; + k = 0; + while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) + k++; + if (k == savedGroup.properties.length) { + property.id = 0; + return; + } + var savedProperty = savedGroup.properties[k]; + property.id = savedProperty.id; + }); + }); + } + }).then(function (data) { + //success + syncTreeNode(vm.contentType, data.path); + vm.page.saveButtonState = 'success'; + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } else { + localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { + localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } + vm.page.saveButtonState = 'error'; + deferred.reject(err); + }); + return deferred.promise; + } + } + function init(contentType) { + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); + }); + } + // convert legacy icons + convertLegacyIcons(contentType); + //set a shared state + editorState.set(contentType); + vm.contentType = contentType; + } + function convertLegacyIcons(contentType) { + // make array to store contentType icon + var contentTypeArray = []; + // push icon to array + contentTypeArray.push({ 'icon': contentType.icon }); + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + } + function getDataTypeDetails(property) { + if (property.propertyState !== 'init') { + dataTypeResource.getById(property.dataTypeId).then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + } + } + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { + navigationService.syncTree({ + tree: 'membertypes', + path: path.split(','), + forceReload: initialLoad !== true + }).then(function (syncArgs) { + vm.currentNode = syncArgs.node; + }); + } + } + angular.module('umbraco').controller('Umbraco.Editors.MemberTypes.EditController', MemberTypesEditController); + }()); + angular.module('umbraco').controller('Umbraco.Editors.MemberTypes.MoveController', function ($scope) { + }); + /** + * @ngdoc controller + * @name Umbraco.Editors.Packages.DeleteController + * @function + * + * @description + * The controller for deleting content + */ + function PackageDeleteController($scope, packageResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + packageResource.deleteCreatedPackage($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Packages.DeleteController', PackageDeleteController); + (function () { + 'use strict'; + function PackagesOverviewController($scope, $route, $location, navigationService, $timeout, localStorageService) { + //Hack! + // if there is a cookie value for packageInstallUri then we need to redirect there, + // the issue is that we still have webforms and we cannot go to a hash location and then window.reload + // because it will double load it. + // we will refresh and then navigate there. + var installPackageUri = localStorageService.get('packageInstallUri'); + if (installPackageUri) { + localStorageService.remove('packageInstallUri'); + } + if (installPackageUri && installPackageUri !== 'installed') { + //navigate to the custom installer screen, if it is just "installed", then we'll + //show the installed view + $location.path(installPackageUri).search(''); + } else { + var vm = this; + vm.page = {}; + vm.page.name = 'Packages'; + vm.page.navigation = [ + { + 'name': 'Packages', + 'icon': 'icon-cloud', + 'view': 'views/packager/views/repo.html', + 'active': !installPackageUri || installPackageUri === 'navigation' + }, + { + 'name': 'Installed', + 'icon': 'icon-box', + 'view': 'views/packager/views/installed.html', + 'active': installPackageUri === 'installed' + }, + { + 'name': 'Install local', + 'icon': 'icon-add', + 'view': 'views/packager/views/install-local.html', + 'active': installPackageUri === 'local' + } + ]; + $timeout(function () { + navigationService.syncTree({ + tree: 'packager', + path: '-1' + }); + }); + } + } + angular.module('umbraco').controller('Umbraco.Editors.Packages.OverviewController', PackagesOverviewController); + }()); + (function () { + 'use strict'; + function PackagesInstallLocalController($scope, $route, $location, Upload, umbRequestHelper, packageResource, localStorageService, $timeout, $window, localizationService, $q) { + var vm = this; + vm.state = 'upload'; + vm.localPackage = {}; + vm.installPackage = installPackage; + vm.installState = { + status: '', + progress: 0 + }; + vm.installCompleted = false; + vm.zipFile = { + uploadStatus: 'idle', + uploadProgress: 0, + serverErrorMessage: null + }; + $scope.handleFiles = function (files, event) { + if (files) { + for (var i = 0; i < files.length; i++) { + upload(files[i]); + } + } + }; + function upload(file) { + Upload.upload({ + url: umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'UploadLocalPackage'), + fields: {}, + file: file + }).progress(function (evt) { + // hack: in some browsers the progress event is called after success + // this prevents the UI from going back to a uploading state + if (vm.zipFile.uploadStatus !== 'done' && vm.zipFile.uploadStatus !== 'error') { + // set view state to uploading + vm.state = 'uploading'; + // calculate progress in percentage + var progressPercentage = parseInt(100 * evt.loaded / evt.total, 10); + // set percentage property on file + vm.zipFile.uploadProgress = progressPercentage; + // set uploading status on file + vm.zipFile.uploadStatus = 'uploading'; + } + }).success(function (data, status, headers, config) { + if (data.notifications && data.notifications.length > 0) { + // set error status on file + vm.zipFile.uploadStatus = 'error'; + // Throw message back to user with the cause of the error + vm.zipFile.serverErrorMessage = data.notifications[0].message; + } else { + // set done status on file + vm.zipFile.uploadStatus = 'done'; + loadPackage(); + vm.zipFile.uploadProgress = 100; + vm.localPackage = data; + } + }).error(function (evt, status, headers, config) { + // set status done + vm.zipFile.uploadStatus = 'error'; + // If file not found, server will return a 404 and display this message + if (status === 404) { + vm.zipFile.serverErrorMessage = 'File not found'; + } else if (status == 400) { + //it's a validation error + vm.zipFile.serverErrorMessage = evt.message; + } else { + //it's an unhandled error + //if the service returns a detailed error + if (evt.InnerException) { + vm.zipFile.serverErrorMessage = evt.InnerException.ExceptionMessage; + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf('ValidateRequestEntityLength') > 0) { + vm.zipFile.serverErrorMessage = 'File too large to upload'; + } + } else if (evt.Message) { + vm.zipFile.serverErrorMessage = evt.Message; + } + } + }); + } + function loadPackage() { + if (vm.zipFile.uploadStatus === 'done') { + vm.state = 'packageDetails'; + } + } + function installPackage() { + vm.installState.status = localizationService.localize('packager_installStateImporting'); + vm.installState.progress = '0'; + packageResource.import(vm.localPackage).then(function (pack) { + vm.installState.progress = '25'; + vm.installState.status = localizationService.localize('packager_installStateInstalling'); + return packageResource.installFiles(pack); + }, installError).then(function (pack) { + vm.installState.status = localizationService.localize('packager_installStateRestarting'); + vm.installState.progress = '50'; + var deferred = $q.defer(); + //check if the app domain is restarted ever 2 seconds + var count = 0; + function checkRestart() { + $timeout(function () { + packageResource.checkRestart(pack).then(function (d) { + count++; + //if there is an id it means it's not restarted yet but we'll limit it to only check 10 times + if (d.isRestarting && count < 10) { + checkRestart(); + } else { + //it's restarted! + deferred.resolve(d); + } + }, installError); + }, 2000); + } + checkRestart(); + return deferred.promise; + }, installError).then(function (pack) { + vm.installState.status = localizationService.localize('packager_installStateRestarting'); + vm.installState.progress = '75'; + return packageResource.installData(pack); + }, installError).then(function (pack) { + vm.installState.status = localizationService.localize('packager_installStateComplete'); + vm.installState.progress = '100'; + return packageResource.cleanUp(pack); + }, installError).then(function (result) { + if (result.postInstallationPath) { + //Put the redirect Uri in a cookie so we can use after reloading + localStorageService.set('packageInstallUri', result.postInstallationPath); + } else { + //set to a constant value so it knows to just go to the installed view + localStorageService.set('packageInstallUri', 'installed'); + } + vm.installState.status = localizationService.localize('packager_installStateCompleted'); + vm.installCompleted = true; + }, installError); + } + function installError() { + //This will return a rejection meaning that the promise change above will stop + return $q.reject(); + } + vm.reloadPage = function () { + //reload on next digest (after cookie) + $timeout(function () { + $window.location.reload(true); + }); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Packages.InstallLocalController', PackagesInstallLocalController); + }()); + (function () { + 'use strict'; + function PackagesInstalledController($scope, $route, $location, packageResource, $timeout, $window, localStorageService, localizationService) { + var vm = this; + vm.confirmUninstall = confirmUninstall; + vm.uninstallPackage = uninstallPackage; + vm.state = 'list'; + vm.installState = { status: '' }; + vm.package = {}; + function init() { + packageResource.getInstalled().then(function (packs) { + vm.installedPackages = packs; + }); + vm.installState.status = ''; + vm.state = 'list'; + } + function confirmUninstall(pck) { + vm.state = 'packageDetails'; + vm.package = pck; + } + function uninstallPackage(installedPackage) { + vm.installState.status = localizationService.localize('packager_installStateUninstalling'); + vm.installState.progress = '0'; + packageResource.uninstall(installedPackage.id).then(function () { + if (installedPackage.files.length > 0) { + vm.installState.status = localizationService.localize('packager_installStateComplete'); + vm.installState.progress = '100'; + //set this flag so that on refresh it shows the installed packages list + localStorageService.set('packageInstallUri', 'installed'); + //reload on next digest (after cookie) + $timeout(function () { + $window.location.reload(true); + }); + } else { + init(); + } + }); + } + init(); + } + angular.module('umbraco').controller('Umbraco.Editors.Packages.InstalledController', PackagesInstalledController); + }()); + (function () { + 'use strict'; + function PackagesRepoController($scope, $route, $location, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { + var vm = this; + vm.packageViewState = 'packageList'; + vm.categories = []; + vm.loading = true; + vm.pagination = { + pageNumber: 1, + totalPages: 10, + pageSize: 24 + }; + vm.searchQuery = ''; + vm.installState = { + status: '', + progress: 0, + type: 'ok' + }; + vm.selectCategory = selectCategory; + vm.showPackageDetails = showPackageDetails; + vm.setPackageViewState = setPackageViewState; + vm.nextPage = nextPage; + vm.prevPage = prevPage; + vm.goToPage = goToPage; + vm.installPackage = installPackage; + vm.downloadPackage = downloadPackage; + vm.openLightbox = openLightbox; + vm.closeLightbox = closeLightbox; + vm.search = search; + vm.installCompleted = false; + var currSort = 'Latest'; + //used to cancel any request in progress if another one needs to take it's place + var canceler = null; + function getActiveCategory() { + if (vm.searchQuery !== '') { + return ''; + } + for (var i = 0; i < vm.categories.length; i++) { + if (vm.categories[i].active === true) { + return vm.categories[i].name; + } + } + return ''; + } + function init() { + vm.loading = true; + $q.all([ + ourPackageRepositoryResource.getCategories().then(function (cats) { + vm.categories = cats; + }), + ourPackageRepositoryResource.getPopular(8).then(function (pack) { + vm.popular = pack.packages; + }), + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort).then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + }) + ]).then(function () { + vm.loading = false; + }); + } + function selectCategory(selectedCategory, categories) { + var reset = false; + for (var i = 0; i < categories.length; i++) { + var category = categories[i]; + if (category.name === selectedCategory.name && category.active === true) { + //it's already selected, let's unselect to show all again + reset = true; + } + category.active = false; + } + vm.loading = true; + vm.searchQuery = ''; + var searchCategory = selectedCategory.name; + if (reset === true) { + searchCategory = ''; + } + currSort = 'Latest'; + $q.all([ + ourPackageRepositoryResource.getPopular(8, searchCategory).then(function (pack) { + vm.popular = pack.packages; + }), + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, searchCategory, vm.searchQuery).then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + vm.pagination.pageNumber = 1; + }) + ]).then(function () { + vm.loading = false; + selectedCategory.active = reset === false; + }); + } + function showPackageDetails(selectedPackage) { + ourPackageRepositoryResource.getDetails(selectedPackage.id).then(function (pack) { + packageResource.validateInstalled(pack.name, pack.latestVersion).then(function () { + //ok, can install + vm.package = pack; + vm.package.isValid = true; + vm.packageViewState = 'packageDetails'; + }, function () { + //nope, cannot install + vm.package = pack; + vm.package.isValid = false; + vm.packageViewState = 'packageDetails'; + }); + }); + } + function setPackageViewState(state) { + if (state) { + vm.packageViewState = state; + } + } + function nextPage(pageNumber) { + ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery).then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + }); + } + function prevPage(pageNumber) { + ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery).then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + }); + } + function goToPage(pageNumber) { + ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery).then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + }); + } + function downloadPackage(selectedPackage) { + vm.loading = true; + packageResource.fetch(selectedPackage.id).then(function (pack) { + vm.packageViewState = 'packageInstall'; + vm.loading = false; + vm.localPackage = pack; + vm.localPackage.allowed = true; + }, function (evt, status, headers, config) { + if (status == 400) { + //it's a validation error + vm.installState.type = 'error'; + vm.zipFile.serverErrorMessage = evt.message; + } + }); + } + function error(e, args) { + //This will return a rejection meaning that the promise change above will stop + return $q.reject(); + } + function installPackage(selectedPackage) { + vm.installState.status = localizationService.localize('packager_installStateImporting'); + vm.installState.progress = '0'; + packageResource.import(selectedPackage).then(function (pack) { + vm.installState.status = localizationService.localize('packager_installStateInstalling'); + vm.installState.progress = '25'; + return packageResource.installFiles(pack); + }, error).then(function (pack) { + vm.installState.status = localizationService.localize('packager_installStateRestarting'); + vm.installState.progress = '50'; + var deferred = $q.defer(); + //check if the app domain is restarted ever 2 seconds + var count = 0; + function checkRestart() { + $timeout(function () { + packageResource.checkRestart(pack).then(function (d) { + count++; + //if there is an id it means it's not restarted yet but we'll limit it to only check 10 times + if (d.isRestarting && count < 10) { + checkRestart(); + } else { + //it's restarted! + deferred.resolve(d); + } + }, error); + }, 2000); + } + checkRestart(); + return deferred.promise; + }, error).then(function (pack) { + vm.installState.status = localizationService.localize('packager_installStateRestarting'); + vm.installState.progress = '75'; + return packageResource.installData(pack); + }, error).then(function (pack) { + vm.installState.status = localizationService.localize('packager_installStateComplete'); + vm.installState.progress = '100'; + return packageResource.cleanUp(pack); + }, error).then(function (result) { + if (result.postInstallationPath) { + //Put the redirect Uri in a cookie so we can use after reloading + localStorageService.set('packageInstallUri', result.postInstallationPath); + } + vm.installState.status = localizationService.localize('packager_installStateCompleted'); + vm.installCompleted = true; + }, error); + } + function openLightbox(itemIndex, items) { + vm.lightbox = { + show: true, + items: items, + activeIndex: itemIndex + }; + } + function closeLightbox() { + vm.lightbox.show = false; + vm.lightbox = null; + } + var searchDebounced = _.debounce(function (e) { + $scope.$apply(function () { + //a canceler exists, so perform the cancelation operation and reset + if (canceler) { + canceler.resolve(); + canceler = $q.defer(); + } else { + canceler = $q.defer(); + } + currSort = vm.searchQuery ? 'Default' : 'Latest'; + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, '', vm.searchQuery, canceler).then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + vm.pagination.pageNumber = 1; + vm.loading = false; + //set back to null so it can be re-created + canceler = null; + }); + }); + }, 200); + function search(searchQuery) { + vm.loading = true; + searchDebounced(); + } + vm.reloadPage = function () { + //reload on next digest (after cookie) + $timeout(function () { + window.location.reload(true); + }); + }; + init(); + } + angular.module('umbraco').controller('Umbraco.Editors.Packages.RepoController', PackagesRepoController); + }()); + (function () { + 'use strict'; + function PartialViewMacrosCreateController($scope, codefileResource, macroResource, $location, navigationService, formHelper, localizationService, appState) { + var vm = this; + var node = $scope.dialogOptions.currentNode; + var localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); + vm.snippets = []; + vm.createFolderError = ''; + vm.folderName = ''; + vm.fileName = ''; + vm.showSnippets = false; + vm.creatingFolder = false; + vm.showCreateFolder = showCreateFolder; + vm.createFolder = createFolder; + vm.createFile = createFile; + vm.createFileWithoutMacro = createFileWithoutMacro; + vm.showCreateFromSnippet = showCreateFromSnippet; + vm.createFileFromSnippet = createFileFromSnippet; + function onInit() { + codefileResource.getSnippets('partialViewMacros').then(function (snippets) { + vm.snippets = snippets; + }); + } + function showCreateFolder() { + vm.creatingFolder = true; + } + function createFolder(form) { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: form, + statusMessage: localizeCreateFolder + })) { + codefileResource.createContainer('partialViewMacros', node.id, vm.folderName).then(function (saved) { + navigationService.hideMenu(); + navigationService.syncTree({ + tree: 'partialViewMacros', + path: saved.path, + forceReload: true, + activate: true + }); + formHelper.resetForm({ scope: $scope }); + var section = appState.getSectionState('currentSection'); + }, function (err) { + vm.createFolderError = err; + //show any notifications + formHelper.showNotifications(err.data); + }); + } + } + function createFile() { + $location.path('/developer/partialviewmacros/edit/' + node.id).search('create', 'true'); + navigationService.hideMenu(); + } + function createFileWithoutMacro() { + $location.path('/developer/partialviewmacros/edit/' + node.id).search('create', 'true').search('nomacro', 'true'); + navigationService.hideMenu(); + } + function createFileFromSnippet(snippet) { + $location.path('/developer/partialviewmacros/edit/' + node.id).search('create', 'true').search('snippet', snippet.fileName); + navigationService.hideMenu(); + } + function showCreateFromSnippet() { + vm.showSnippets = true; + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Editors.PartialViewMacros.CreateController', PartialViewMacrosCreateController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.PartialViewMacros.DeleteController + * @function + * + * @description + * The controller for deleting partial view macros + */ + function PartialViewMacrosDeleteController($scope, codefileResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + codefileResource.deleteByPath('partialViewMacros', $scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.PartialViewMacros.DeleteController', PartialViewMacrosDeleteController); + (function () { + 'use strict'; + function partialViewMacrosEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, macroResource) { + var vm = this; + var localizeSaving = localizationService.localize('general_saving'); + vm.page = {}; + vm.page.loading = true; + vm.partialViewMacroFile = {}; + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState('currentSection'); + vm.page.menu.currentNode = null; + // bind functions to view model + vm.save = save; + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; + /* Functions bound to view model */ + function save() { + vm.page.saveButtonState = 'busy'; + vm.partialViewMacro.content = vm.editor.getValue(); + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: codefileResource.save, + scope: $scope, + content: vm.partialViewMacro, + // We do not redirect on failure for partial view macros - this is because it is not possible to actually save the partial view + // when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) { + } + }).then(function (saved) { + // create macro if needed + if ($routeParams.create && $routeParams.nomacro !== 'true') { + macroResource.createPartialViewMacroWithFile(saved.virtualPath, saved.name).then(function (created) { + completeSave(saved); + }, function (err) { + //show any notifications + formHelper.showNotifications(err.data); + }); + } else { + completeSave(saved); + } + }, function (err) { + vm.page.saveButtonState = 'error'; + localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { + localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + }); + } + function completeSave(saved) { + localizationService.localize('speechBubbles_partialViewSavedHeader').then(function (headerValue) { + localizationService.localize('speechBubbles_partialViewSavedText').then(function (msgValue) { + notificationsService.success(headerValue, msgValue); + }); + }); + //check if the name changed, if so we need to redirect + if (vm.partialViewMacro.id !== saved.id) { + contentEditingHelper.redirectToRenamedContent(saved.id); + } else { + vm.page.saveButtonState = 'success'; + vm.partialViewMacro = saved; + //sync state + editorState.set(vm.partialViewMacro); + // normal tree sync + navigationService.syncTree({ + tree: 'partialViewMacros', + path: vm.partialViewMacro.path, + forceReload: true + }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + // clear $dirty state on form + setFormState('pristine'); + } + } + function openInsertOverlay() { + vm.insertOverlay = { + view: 'insert', + allowedTypes: { + macro: true, + dictionary: true, + umbracoField: true + }, + hideSubmitButton: true, + show: true, + submit: function (model) { + switch (model.insert.type) { + case 'macro': + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, 'Mvc'); + insert(macroObject.syntax); + break; + case 'dictionary': + var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); + insert(code); + break; + case 'umbracoField': + insert(model.insert.umbracoField); + break; + } + vm.insertOverlay.show = false; + vm.insertOverlay = null; + }, + close: function (oldModel) { + // close the dialog + vm.insertOverlay.show = false; + vm.insertOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openMacroOverlay() { + vm.macroPickerOverlay = { + view: 'macropicker', + dialogData: {}, + show: true, + title: 'Insert macro', + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, 'Mvc'); + insert(macroObject.syntax); + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + }, + close: function (oldModel) { + // close the dialog + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openPageFieldOverlay() { + vm.pageFieldOverlay = { + submitButtonLabel: 'Insert', + closeButtonlabel: 'Cancel', + view: 'insertfield', + show: true, + submit: function (model) { + insert(model.umbracoField); + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + }, + close: function (model) { + // close the dialog + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openDictionaryItemOverlay() { + vm.dictionaryItemOverlay = { + view: 'treepicker', + section: 'settings', + treeAlias: 'dictionary', + entityType: 'dictionary', + multiPicker: false, + show: true, + title: 'Insert dictionary item', + emptyStateMessage: localizationService.localize('emptyStates_emptyDictionaryTree'), + select: function (node) { + var code = templateHelper.getInsertDictionarySnippet(node.name); + insert(code); + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + }, + close: function (model) { + // close dialog + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openQueryBuilderOverlay() { + vm.queryBuilderOverlay = { + view: 'querybuilder', + show: true, + title: 'Query for content', + submit: function (model) { + var code = templateHelper.getQuerySnippet(model.result.queryExpression); + insert(code); + vm.queryBuilderOverlay.show = false; + vm.queryBuilderOverlay = null; + }, + close: function (model) { + // close dialog + vm.queryBuilderOverlay.show = false; + vm.queryBuilderOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + /* Local functions */ + function init() { + //we need to load this somewhere, for now its here. + assetsService.loadCss('lib/ace-razor-mode/theme/razor_chrome.css'); + if ($routeParams.create) { + var snippet = 'Empty'; + if ($routeParams.snippet) { + snippet = $routeParams.snippet; + } + codefileResource.getScaffold('partialViewMacros', $routeParams.id, snippet).then(function (partialViewMacro) { + if ($routeParams.name) { + partialViewMacro.name = $routeParams.name; + } + ready(partialViewMacro, false); + }); + } else { + codefileResource.getByPath('partialViewMacros', $routeParams.id).then(function (partialViewMacro) { + ready(partialViewMacro, true); + }); + } + } + function ready(partialViewMacro, syncTree) { + vm.page.loading = false; + vm.partialViewMacro = partialViewMacro; + //sync state + editorState.set(vm.partialViewMacro); + if (syncTree) { + navigationService.syncTree({ + tree: 'partialViewMacros', + path: vm.partialViewMacro.path, + forceReload: true + }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + // ace configuration + vm.aceOption = { + mode: 'razor', + theme: 'chrome', + showPrintMargin: false, + advanced: { fontSize: '14px' }, + onLoad: function (_editor) { + vm.editor = _editor; + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if (!$routeParams.create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + //change on blur, focus + vm.editor.on('blur', persistCurrentLocation); + vm.editor.on('focus', persistCurrentLocation); + vm.editor.on('change', changeAceEditor); + } + }; + } + function insert(str) { + vm.editor.focus(); + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + // set form state to $dirty + setFormState('dirty'); + } + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + function changeAceEditor() { + setFormState('dirty'); + } + function setFormState(state) { + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + // set state + if (state === 'dirty') { + currentForm.$setDirty(); + } else if (state === 'pristine') { + currentForm.$setPristine(); + } + } + init(); + } + angular.module('umbraco').controller('Umbraco.Editors.PartialViewMacros.EditController', partialViewMacrosEditController); + }()); + (function () { + 'use strict'; + function PartialViewsCreateController($scope, codefileResource, $location, navigationService, formHelper, localizationService, appState) { + var vm = this; + var node = $scope.dialogOptions.currentNode; + var localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); + vm.snippets = []; + vm.showSnippets = false; + vm.creatingFolder = false; + vm.createFolderError = ''; + vm.folderName = ''; + vm.createPartialView = createPartialView; + vm.showCreateFolder = showCreateFolder; + vm.createFolder = createFolder; + vm.showCreateFromSnippet = showCreateFromSnippet; + function onInit() { + codefileResource.getSnippets('partialViews').then(function (snippets) { + vm.snippets = snippets; + }); + } + function createPartialView(selectedSnippet) { + var snippet = null; + if (selectedSnippet && selectedSnippet.fileName) { + snippet = selectedSnippet.fileName; + } + $location.path('/settings/partialviews/edit/' + node.id).search('create', 'true').search('snippet', snippet); + navigationService.hideMenu(); + } + function showCreateFolder() { + vm.creatingFolder = true; + } + function createFolder(form) { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: form, + statusMessage: localizeCreateFolder + })) { + codefileResource.createContainer('partialViews', node.id, vm.folderName).then(function (saved) { + navigationService.hideMenu(); + navigationService.syncTree({ + tree: 'partialViews', + path: saved.path, + forceReload: true, + activate: true + }); + formHelper.resetForm({ scope: $scope }); + var section = appState.getSectionState('currentSection'); + }, function (err) { + vm.createFolderError = err; + formHelper.showNotifications(err.data); + }); + } + } + function showCreateFromSnippet() { + vm.showSnippets = true; + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Editors.PartialViews.CreateController', PartialViewsCreateController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.PartialViews.DeleteController + * @function + * + * @description + * The controller for deleting partial views + */ + function PartialViewsDeleteController($scope, codefileResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + codefileResource.deleteByPath('partialViews', $scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.PartialViews.DeleteController', PartialViewsDeleteController); + (function () { + 'use strict'; + function PartialViewsEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper) { + var vm = this; + var localizeSaving = localizationService.localize('general_saving'); + vm.page = {}; + vm.page.loading = true; + vm.partialView = {}; + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState('currentSection'); + vm.page.menu.currentNode = null; + //Used to toggle the keyboard shortcut modal + //From a custom keybinding in ace editor - that conflicts with our own to show the dialog + vm.showKeyboardShortcut = false; + //Keyboard shortcuts for help dialog + vm.page.keyboardShortcutsOverview = []; + vm.page.keyboardShortcutsOverview.push(templateHelper.getGeneralShortcuts()); + vm.page.keyboardShortcutsOverview.push(templateHelper.getEditorShortcuts()); + vm.page.keyboardShortcutsOverview.push(templateHelper.getPartialViewEditorShortcuts()); + // bind functions to view model + vm.save = save; + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; + /* Functions bound to view model */ + function save() { + vm.page.saveButtonState = 'busy'; + vm.partialView.content = vm.editor.getValue(); + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: codefileResource.save, + scope: $scope, + content: vm.partialView, + //We do not redirect on failure for partialviews - this is because it is not possible to actually save the partialviews + // type when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) { + } + }).then(function (saved) { + localizationService.localize('speechBubbles_partialViewSavedHeader').then(function (headerValue) { + localizationService.localize('speechBubbles_partialViewSavedText').then(function (msgValue) { + notificationsService.success(headerValue, msgValue); + }); + }); + //check if the name changed, if so we need to redirect + if (vm.partialView.id !== saved.id) { + contentEditingHelper.redirectToRenamedContent(saved.id); + } else { + vm.page.saveButtonState = 'success'; + vm.partialView = saved; + //sync state + editorState.set(vm.partialView); + // normal tree sync + navigationService.syncTree({ + tree: 'partialViews', + path: vm.partialView.path, + forceReload: true + }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + // clear $dirty state on form + setFormState('pristine'); + } + }, function (err) { + vm.page.saveButtonState = 'error'; + localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { + localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + }); + } + function openInsertOverlay() { + vm.insertOverlay = { + view: 'insert', + allowedTypes: { + macro: true, + dictionary: true, + umbracoField: true + }, + hideSubmitButton: true, + show: true, + submit: function (model) { + switch (model.insert.type) { + case 'macro': + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, 'Mvc'); + insert(macroObject.syntax); + break; + case 'dictionary': + var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); + insert(code); + break; + case 'umbracoField': + insert(model.insert.umbracoField); + break; + } + vm.insertOverlay.show = false; + vm.insertOverlay = null; + }, + close: function (oldModel) { + // close the dialog + vm.insertOverlay.show = false; + vm.insertOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openMacroOverlay() { + vm.macroPickerOverlay = { + view: 'macropicker', + dialogData: {}, + show: true, + title: 'Insert macro', + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, 'Mvc'); + insert(macroObject.syntax); + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + }, + close: function (oldModel) { + // close the dialog + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openPageFieldOverlay() { + vm.pageFieldOverlay = { + submitButtonLabel: 'Insert', + closeButtonlabel: 'Cancel', + view: 'insertfield', + show: true, + submit: function (model) { + insert(model.umbracoField); + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + }, + close: function (model) { + // close the dialog + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openDictionaryItemOverlay() { + vm.dictionaryItemOverlay = { + view: 'treepicker', + section: 'settings', + treeAlias: 'dictionary', + entityType: 'dictionary', + multiPicker: false, + show: true, + title: 'Insert dictionary item', + emptyStateMessage: localizationService.localize('emptyStates_emptyDictionaryTree'), + select: function (node) { + var code = templateHelper.getInsertDictionarySnippet(node.name); + insert(code); + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + }, + close: function (model) { + // close dialog + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openQueryBuilderOverlay() { + vm.queryBuilderOverlay = { + view: 'querybuilder', + show: true, + title: 'Query for content', + submit: function (model) { + var code = templateHelper.getQuerySnippet(model.result.queryExpression); + insert(code); + vm.queryBuilderOverlay.show = false; + vm.queryBuilderOverlay = null; + }, + close: function (model) { + // close dialog + vm.queryBuilderOverlay.show = false; + vm.queryBuilderOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + /* Local functions */ + function init() { + //we need to load this somewhere, for now its here. + assetsService.loadCss('lib/ace-razor-mode/theme/razor_chrome.css'); + if ($routeParams.create) { + var snippet = 'Empty'; + if ($routeParams.snippet) { + snippet = $routeParams.snippet; + } + codefileResource.getScaffold('partialViews', $routeParams.id, snippet).then(function (partialView) { + ready(partialView, false); + }); + } else { + codefileResource.getByPath('partialViews', $routeParams.id).then(function (partialView) { + ready(partialView, true); + }); + } + } + function ready(partialView, syncTree) { + vm.page.loading = false; + vm.partialView = partialView; + //sync state + editorState.set(vm.partialView); + if (syncTree) { + navigationService.syncTree({ + tree: 'partialViews', + path: vm.partialView.path, + forceReload: true + }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + // ace configuration + vm.aceOption = { + mode: 'razor', + theme: 'chrome', + showPrintMargin: false, + advanced: { fontSize: '14px' }, + onLoad: function (_editor) { + vm.editor = _editor; + //Update the auto-complete method to use ctrl+alt+space + _editor.commands.bindKey('ctrl-alt-space', 'startAutocomplete'); + //Unassigns the keybinding (That was previously auto-complete) + //As conflicts with our own tree search shortcut + _editor.commands.bindKey('ctrl-space', null); + // Assign new keybinding + _editor.commands.addCommands([ + //Disable (alt+shift+K) + //Conflicts with our own show shortcuts dialog - this overrides it + { + name: 'unSelectOrFindPrevious', + bindKey: 'Alt-Shift-K', + exec: function () { + //Toggle the show keyboard shortcuts overlay + $scope.$apply(function () { + vm.showKeyboardShortcut = !vm.showKeyboardShortcut; + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoValue', + bindKey: 'Alt-Shift-V', + exec: function () { + $scope.$apply(function () { + openPageFieldOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertDictionary', + bindKey: 'Alt-Shift-D', + exec: function () { + $scope.$apply(function () { + openDictionaryItemOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoMacro', + bindKey: 'Alt-Shift-M', + exec: function () { + $scope.$apply(function () { + openMacroOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertQuery', + bindKey: 'Alt-Shift-Q', + exec: function () { + $scope.$apply(function () { + openQueryBuilderOverlay(); + }); + }, + readOnly: true + } + ]); + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if (!$routeParams.create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + //change on blur, focus + vm.editor.on('blur', persistCurrentLocation); + vm.editor.on('focus', persistCurrentLocation); + vm.editor.on('change', changeAceEditor); + } + }; + } + function insert(str) { + vm.editor.focus(); + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + // set form state to $dirty + setFormState('dirty'); + } + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + function changeAceEditor() { + setFormState('dirty'); + } + function setFormState(state) { + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + // set state + if (state === 'dirty') { + currentForm.$setDirty(); + } else if (state === 'pristine') { + currentForm.$setPristine(); + } + } + init(); + } + angular.module('umbraco').controller('Umbraco.Editors.PartialViews.EditController', PartialViewsEditController); + }()); + function imageFilePickerController($scope) { + $scope.pick = function () { + $scope.mediaPickerDialog = {}; + $scope.mediaPickerDialog.view = 'mediapicker'; + $scope.mediaPickerDialog.show = true; + $scope.mediaPickerDialog.submit = function (model) { + $scope.model.value = model.selectedImages[0].image; + $scope.mediaPickerDialog.show = false; + $scope.mediaPickerDialog = null; + }; + $scope.mediaPickerDialog.close = function (oldModel) { + $scope.mediaPickerDialog.show = false; + $scope.mediaPickerDialog = null; + }; + }; + } + angular.module('umbraco').controller('Umbraco.PrevalueEditors.ImageFilePickerController', imageFilePickerController); + //this controller simply tells the dialogs service to open a mediaPicker window + //with a specified callback, this callback will receive an object with a selection on it + function mediaPickerController($scope, dialogService, entityResource, $log, iconHelper) { + function trim(str, chr) { + var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); + return str.replace(rgxtrim, ''); + } + $scope.renderModel = []; + $scope.allowRemove = true; + $scope.allowEdit = true; + $scope.sortable = false; + var dialogOptions = { + multiPicker: false, + entityType: 'Media', + section: 'media', + treeAlias: 'media', + idType: 'int' + }; + //combine the dialogOptions with any values returned from the server + if ($scope.model.config) { + angular.extend(dialogOptions, $scope.model.config); + } + $scope.openContentPicker = function () { + $scope.contentPickerOverlay = dialogOptions; + $scope.contentPickerOverlay.view = 'treePicker'; + $scope.contentPickerOverlay.show = true; + $scope.contentPickerOverlay.submit = function (model) { + if ($scope.contentPickerOverlay.multiPicker) { + _.each(model.selection, function (item, i) { + $scope.add(item); + }); + } else { + $scope.clear(); + $scope.add(model.selection[0]); + } + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + $scope.contentPickerOverlay.close = function (oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + }; + $scope.remove = function (index) { + $scope.renderModel.splice(index, 1); + }; + $scope.clear = function () { + $scope.renderModel = []; + }; + $scope.add = function (item) { + var itemId = dialogOptions.idType === 'udi' ? item.udi : item.id; + var currIds = _.map($scope.renderModel, function (i) { + return dialogOptions.idType === 'udi' ? i.udi : i.id; + }); + if (currIds.indexOf(itemId) < 0) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.renderModel.push({ + name: item.name, + id: item.id, + icon: item.icon, + udi: item.udi + }); + // store the index of the new item in the renderModel collection so we can find it again + var itemRenderIndex = $scope.renderModel.length - 1; + // get and update the path for the picked node + entityResource.getUrl(item.id, dialogOptions.entityType).then(function (data) { + $scope.renderModel[itemRenderIndex].path = data; + }); + } + }; + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + var currIds = _.map($scope.renderModel, function (i) { + return dialogOptions.idType === 'udi' ? i.udi : i.id; + }); + $scope.model.value = trim(currIds.join(), ','); + }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + //load media data + var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; + if (modelIds.length > 0) { + entityResource.getByIds(modelIds, dialogOptions.entityType).then(function (data) { + _.each(data, function (item, i) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.renderModel.push({ + name: item.name, + id: item.id, + icon: item.icon, + udi: item.udi + }); + // store the index of the new item in the renderModel collection so we can find it again + var itemRenderIndex = $scope.renderModel.length - 1; + // get and update the path for the picked node + entityResource.getUrl(item.id, dialogOptions.entityType).then(function (data) { + $scope.renderModel[itemRenderIndex].path = data; + }); + }); + }); + } + } + angular.module('umbraco').controller('Umbraco.PrevalueEditors.MediaPickerController', mediaPickerController); + angular.module('umbraco').controller('Umbraco.PrevalueEditors.MultiValuesController', function ($scope, $timeout) { + //NOTE: We need to make each item an object, not just a string because you cannot 2-way bind to a primitive. + $scope.newItem = ''; + $scope.hasError = false; + if (!angular.isArray($scope.model.value)) { + //make an array from the dictionary + var items = []; + for (var i in $scope.model.value) { + items.push({ + value: $scope.model.value[i].value, + sortOrder: $scope.model.value[i].sortOrder, + id: i + }); + } + //ensure the items are sorted by the provided sort order + items.sort(function (a, b) { + return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; + }); + //now make the editor model the array + $scope.model.value = items; + } + $scope.remove = function (item, evt) { + evt.preventDefault(); + $scope.model.value = _.reject($scope.model.value, function (x) { + return x.value === item.value; + }); + }; + $scope.add = function (evt) { + evt.preventDefault(); + if ($scope.newItem) { + if (!_.contains($scope.model.value, $scope.newItem)) { + $scope.model.value.push({ value: $scope.newItem }); + $scope.newItem = ''; + $scope.hasError = false; + return; + } + } + //there was an error, do the highlight (will be set back by the directive) + $scope.hasError = true; + }; + $scope.sortableOptions = { + axis: 'y', + containment: 'parent', + cursor: 'move', + items: '> div.control-group', + tolerance: 'pointer', + update: function (e, ui) { + // Get the new and old index for the moved element (using the text as the identifier, so + // we'd have a problem if two prevalues were the same, but that would be unlikely) + var newIndex = ui.item.index(); + var movedPrevalueText = $('input[type="text"]', ui.item).val(); + var originalIndex = getElementIndexByPrevalueText(movedPrevalueText); + // Move the element in the model + if (originalIndex > -1) { + var movedElement = $scope.model.value[originalIndex]; + $scope.model.value.splice(originalIndex, 1); + $scope.model.value.splice(newIndex, 0, movedElement); + } + } + }; + function getElementIndexByPrevalueText(value) { + for (var i = 0; i < $scope.model.value.length; i++) { + if ($scope.model.value[i].value === value) { + return i; + } + } + return -1; + } + }); + //this controller simply tells the dialogs service to open a mediaPicker window + //with a specified callback, this callback will receive an object with a selection on it + angular.module('umbraco').controller('Umbraco.PrevalueEditors.TreePickerController', function ($scope, dialogService, entityResource, $log, iconHelper) { + $scope.renderModel = []; + $scope.ids = []; + $scope.allowRemove = true; + $scope.allowEdit = true; + $scope.sortable = false; + var config = { + multiPicker: false, + entityType: 'Document', + type: 'content', + treeAlias: 'content', + idType: 'int' + }; + //combine the config with any values returned from the server + if ($scope.model.config) { + angular.extend(config, $scope.model.config); + } + if ($scope.model.value) { + $scope.ids = $scope.model.value.split(','); + entityResource.getByIds($scope.ids, config.entityType).then(function (data) { + _.each(data, function (item, i) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.renderModel.push({ + name: item.name, + id: item.id, + icon: item.icon, + udi: item.udi + }); + // store the index of the new item in the renderModel collection so we can find it again + var itemRenderIndex = $scope.renderModel.length - 1; + // get and update the path for the picked node + entityResource.getUrl(item.id, config.entityType).then(function (data) { + $scope.renderModel[itemRenderIndex].path = data; + }); + }); + }); + } + $scope.openContentPicker = function () { + $scope.treePickerOverlay = config; + $scope.treePickerOverlay.section = config.type; + $scope.treePickerOverlay.view = 'treePicker'; + $scope.treePickerOverlay.show = true; + $scope.treePickerOverlay.submit = function (model) { + if (config.multiPicker) { + populate(model.selection); + } else { + populate(model.selection[0]); + } + $scope.treePickerOverlay.show = false; + $scope.treePickerOverlay = null; + }; + $scope.treePickerOverlay.close = function (oldModel) { + $scope.treePickerOverlay.show = false; + $scope.treePickerOverlay = null; + }; + }; + $scope.remove = function (index) { + $scope.renderModel.splice(index, 1); + $scope.ids.splice(index, 1); + $scope.model.value = trim($scope.ids.join(), ','); + }; + $scope.clear = function () { + $scope.model.value = ''; + $scope.renderModel = []; + $scope.ids = []; + }; + $scope.add = function (item) { + var itemId = config.idType === 'udi' ? item.udi : item.id; + if ($scope.ids.indexOf(itemId) < 0) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.ids.push(itemId); + $scope.renderModel.push({ + name: item.name, + id: item.id, + icon: item.icon, + udi: item.udi + }); + $scope.model.value = trim($scope.ids.join(), ','); + // store the index of the new item in the renderModel collection so we can find it again + var itemRenderIndex = $scope.renderModel.length - 1; + // get and update the path for the picked node + entityResource.getUrl(item.id, config.entityType).then(function (data) { + $scope.renderModel[itemRenderIndex].path = data; + }); + } + }; + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + $scope.model.value = trim($scope.ids.join(), ','); + }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + function trim(str, chr) { + var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); + return str.replace(rgxtrim, ''); + } + function populate(data) { + if (angular.isArray(data)) { + _.each(data, function (item, i) { + $scope.add(item); + }); + } else { + $scope.clear(); + $scope.add(data); + } + } + }); + //this controller simply tells the dialogs service to open a mediaPicker window + //with a specified callback, this callback will receive an object with a selection on it + angular.module('umbraco').controller('Umbraco.PrevalueEditors.TreeSourceController', function ($scope, dialogService, entityResource, $log, iconHelper) { + if (!$scope.model) { + $scope.model = {}; + } + if (!$scope.model.value) { + $scope.model.value = { type: 'content' }; + } + if (!$scope.model.config) { + $scope.model.config = { idType: 'int' }; + } + if ($scope.model.value.id && $scope.model.value.type !== 'member') { + var ent = 'Document'; + if ($scope.model.value.type === 'media') { + ent = 'Media'; + } + entityResource.getById($scope.model.value.id, ent).then(function (item) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.node = item; + }); + } + $scope.openContentPicker = function () { + $scope.treePickerOverlay = { + view: 'treepicker', + idType: $scope.model.config.idType, + section: $scope.model.value.type, + treeAlias: $scope.model.value.type, + multiPicker: false, + show: true, + submit: function (model) { + var item = model.selection[0]; + populate(item); + $scope.treePickerOverlay.show = false; + $scope.treePickerOverlay = null; + } + }; + }; + $scope.clear = function () { + $scope.model.value.id = undefined; + $scope.node = undefined; + $scope.model.value.query = undefined; + }; + //we always need to ensure we dont submit anything broken + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + if ($scope.model.value.type === 'member') { + $scope.model.value.id = -1; + $scope.model.value.query = ''; + } + }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + function populate(item) { + $scope.clear(); + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.node = item; + $scope.model.value.id = $scope.model.config.idType === 'udi' ? item.udi : item.id; + } + }); + function booleanEditorController($scope, $rootScope, assetsService) { + function setupViewModel() { + $scope.renderModel = { value: false }; + if ($scope.model.config && $scope.model.config.default && $scope.model.config.default.toString() === '1' && $scope.model && !$scope.model.value) { + $scope.renderModel.value = true; + } + if ($scope.model && $scope.model.value && ($scope.model.value.toString() === '1' || angular.lowercase($scope.model.value) === 'true')) { + $scope.renderModel.value = true; + } + } + setupViewModel(); + $scope.$watch('renderModel.value', function (newVal) { + $scope.model.value = newVal === true ? '1' : '0'; + }); + //here we declare a special method which will be called whenever the value has changed from the server + //this is instead of doing a watch on the model.value = faster + $scope.model.onValueChanged = function (newVal, oldVal) { + //update the display val again if it has changed from the server + setupViewModel(); + }; + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.BooleanController', booleanEditorController); + angular.module('umbraco').controller('Umbraco.PropertyEditors.ChangePasswordController', function ($scope, $routeParams) { + $scope.isNew = $routeParams.create; + function resetModel() { + //the model config will contain an object, if it does not we'll create defaults + //NOTE: We will not support doing the password regex on the client side because the regex on the server side + //based on the membership provider cannot always be ported to js from .net directly. + /* + { + hasPassword: true/false, + requiresQuestionAnswer: true/false, + enableReset: true/false, + enablePasswordRetrieval: true/false, + minPasswordLength: 10 + } + */ + //set defaults if they are not available + if (!$scope.model.config || $scope.model.config.disableToggle === undefined) { + $scope.model.config.disableToggle = false; + } + if (!$scope.model.config || $scope.model.config.hasPassword === undefined) { + $scope.model.config.hasPassword = false; + } + if (!$scope.model.config || $scope.model.config.enablePasswordRetrieval === undefined) { + $scope.model.config.enablePasswordRetrieval = true; + } + if (!$scope.model.config || $scope.model.config.requiresQuestionAnswer === undefined) { + $scope.model.config.requiresQuestionAnswer = false; + } + if (!$scope.model.config || $scope.model.config.enableReset === undefined) { + $scope.model.config.enableReset = true; + } + if (!$scope.model.config || $scope.model.config.minPasswordLength === undefined) { + $scope.model.config.minPasswordLength = 0; + } + //set the model defaults + if (!angular.isObject($scope.model.value)) { + //if it's not an object then just create a new one + $scope.model.value = { + newPassword: null, + oldPassword: null, + reset: null, + answer: null + }; + } else { + //just reset the values + if (!$scope.isNew) { + //if it is new, then leave the generated pass displayed + $scope.model.value.newPassword = null; + $scope.model.value.oldPassword = null; + } + $scope.model.value.reset = null; + $scope.model.value.answer = null; + } + } + resetModel(); + }); + angular.module('umbraco').controller('Umbraco.PropertyEditors.CheckboxListController', function ($scope) { + if (angular.isObject($scope.model.config.items)) { + //now we need to format the items in the dictionary because we always want to have an array + var newItems = []; + var vals = _.values($scope.model.config.items); + var keys = _.keys($scope.model.config.items); + for (var i = 0; i < vals.length; i++) { + newItems.push({ + id: keys[i], + sortOrder: vals[i].sortOrder, + value: vals[i].value + }); + } + //ensure the items are sorted by the provided sort order + newItems.sort(function (a, b) { + return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; + }); + //re-assign + $scope.model.config.items = newItems; + } + function setupViewModel() { + $scope.selectedItems = []; + //now we need to check if the value is null/undefined, if it is we need to set it to "" so that any value that is set + // to "" gets selected by default + if ($scope.model.value === null || $scope.model.value === undefined) { + $scope.model.value = []; + } + for (var i = 0; i < $scope.model.config.items.length; i++) { + var isChecked = _.contains($scope.model.value, $scope.model.config.items[i].id); + $scope.selectedItems.push({ + checked: isChecked, + key: $scope.model.config.items[i].id, + val: $scope.model.config.items[i].value + }); + } + } + setupViewModel(); + //update the model when the items checked changes + $scope.$watch('selectedItems', function (newVal, oldVal) { + $scope.model.value = []; + for (var x = 0; x < $scope.selectedItems.length; x++) { + if ($scope.selectedItems[x].checked) { + $scope.model.value.push($scope.selectedItems[x].key); + } + } + }, true); + //here we declare a special method which will be called whenever the value has changed from the server + //this is instead of doing a watch on the model.value = faster + $scope.model.onValueChanged = function (newVal, oldVal) { + //update the display val again if it has changed from the server + setupViewModel(); + }; + }); + function ColorPickerController($scope) { + $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; + if ($scope.isConfigured) { + for (var key in $scope.model.config.items) { + if (!$scope.model.config.items[key].hasOwnProperty('value')) + $scope.model.config.items[key] = { + value: $scope.model.config.items[key], + label: $scope.model.config.items[key] + }; + } + $scope.model.useLabel = isTrue($scope.model.config.useLabel); + initActiveColor(); + } + $scope.toggleItem = function (color) { + var currentColor = $scope.model.value.hasOwnProperty('value') ? $scope.model.value.value : $scope.model.value; + var newColor; + if (currentColor === color.value) { + // deselect + $scope.model.value = $scope.model.useLabel ? { + value: '', + label: '' + } : ''; + newColor = ''; + } else { + // select + $scope.model.value = $scope.model.useLabel ? { + value: color.value, + label: color.label + } : color.value; + newColor = color.value; + } + // this is required to re-validate + $scope.propertyForm.modelValue.$setViewValue(newColor); + }; + // Method required by the valPropertyValidator directive (returns true if the property editor has at least one color selected) + $scope.validateMandatory = function () { + var isValid = !$scope.model.validation.mandatory || $scope.model.value != null && $scope.model.value != '' && (!$scope.model.value.hasOwnProperty('value') || $scope.model.value.value !== ''); + return { + isValid: isValid, + errorMsg: 'Value cannot be empty', + errorKey: 'required' + }; + }; + $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; + // A color is active if it matches the value and label of the model. + // If the model doesn't store the label, ignore the label during the comparison. + $scope.isActiveColor = function (color) { + // no value + if (!$scope.model.value) + return false; + // Complex color (value and label)? + if (!$scope.model.value.hasOwnProperty('value')) + return $scope.model.value === color.value; + return $scope.model.value.value === color.value && $scope.model.value.label === color.label; + }; + // Finds the color best matching the model's color, + // and sets the model color to that one. This is useful when + // either the value or label was changed on the data type. + function initActiveColor() { + // no value + if (!$scope.model.value) + return; + // Complex color (value and label)? + if (!$scope.model.value.hasOwnProperty('value')) + return; + var modelColor = $scope.model.value.value; + var modelLabel = $scope.model.value.label; + // Check for a full match or partial match. + var foundItem = null; + // Look for a fully matching color. + for (var key in $scope.model.config.items) { + var item = $scope.model.config.items[key]; + if (item.value == modelColor && item.label == modelLabel) { + foundItem = item; + break; + } + } + // Look for a color with a matching value. + if (!foundItem) { + for (var key in $scope.model.config.items) { + var item = $scope.model.config.items[key]; + if (item.value == modelColor) { + foundItem = item; + break; + } + } + } + // Look for a color with a matching label. + if (!foundItem) { + for (var key in $scope.model.config.items) { + var item = $scope.model.config.items[key]; + if (item.label == modelLabel) { + foundItem = item; + break; + } + } + } + // If a match was found, set it as the active color. + if (foundItem) { + $scope.model.value.value = foundItem.value; + $scope.model.value.label = foundItem.label; + } + } + // figures out if a value is trueish enough + function isTrue(bool) { + return !!bool && bool !== '0' && angular.lowercase(bool) !== 'false'; + } + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.ColorPickerController', ColorPickerController); + angular.module('umbraco').controller('Umbraco.PrevalueEditors.MultiColorPickerController', function ($scope, $timeout, assetsService, angularHelper, $element) { + //NOTE: We need to make each color an object, not just a string because you cannot 2-way bind to a primitive. + var defaultColor = '000000'; + var defaultLabel = null; + $scope.newColor = defaultColor; + $scope.newLavel = defaultLabel; + $scope.hasError = false; + assetsService.load([//"lib/spectrum/tinycolor.js", + 'lib/spectrum/spectrum.js'], $scope).then(function () { + var elem = $element.find('input[name=\'newColor\']'); + elem.spectrum({ + color: null, + showInitial: false, + chooseText: 'choose', + // TODO: These can be localised + cancelText: 'cancel', + // TODO: These can be localised + preferredFormat: 'hex', + showInput: true, + clickoutFiresChange: true, + hide: function (color) { + //show the add butotn + $element.find('.btn.add').show(); + }, + change: function (color) { + angularHelper.safeApply($scope, function () { + $scope.newColor = color.toHexString().trimStart('#'); // #ff0000 + }); + }, + show: function () { + //hide the add butotn + $element.find('.btn.add').hide(); + } + }); + }); + if (!angular.isArray($scope.model.value)) { + //make an array from the dictionary + var items = []; + for (var i in $scope.model.value) { + var oldValue = $scope.model.value[i]; + if (oldValue.hasOwnProperty('value')) { + items.push({ + value: oldValue.value, + label: oldValue.label, + id: i + }); + } else { + items.push({ + value: oldValue, + label: oldValue, + id: i + }); + } + } + //now make the editor model the array + $scope.model.value = items; + } + // ensure labels + for (var i = 0; i < $scope.model.value.length; i++) { + var item = $scope.model.value[i]; + item.label = item.hasOwnProperty('label') ? item.label : item.value; + } + function validLabel(label) { + return label !== null && typeof label !== 'undefined' && label !== '' && label.length && label.length > 0; + } + $scope.remove = function (item, evt) { + evt.preventDefault(); + $scope.model.value = _.reject($scope.model.value, function (x) { + return x.value === item.value && x.label === item.label; + }); + }; + $scope.add = function (evt) { + evt.preventDefault(); + if ($scope.newColor) { + var newLabel = validLabel($scope.newLabel) ? $scope.newLabel : $scope.newColor; + var exists = _.find($scope.model.value, function (item) { + return item.value.toUpperCase() === $scope.newColor.toUpperCase() || item.label.toUpperCase() === newLabel.toUpperCase(); + }); + if (!exists) { + $scope.model.value.push({ + value: $scope.newColor, + label: newLabel + }); + $scope.hasError = false; + return; + } + //there was an error, do the highlight (will be set back by the directive) + $scope.hasError = true; + } + }; + //load the separate css for the editor to avoid it blocking our js loading + assetsService.loadCss('lib/spectrum/spectrum.css'); + }); + //this controller simply tells the dialogs service to open a mediaPicker window + //with a specified callback, this callback will receive an object with a selection on it + function contentPickerController($scope, entityResource, editorState, iconHelper, $routeParams, angularHelper, navigationService, $location, miniEditorHelper, localizationService) { + var unsubscribe; + function subscribe() { + unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + var currIds = _.map($scope.renderModel, function (i) { + return $scope.model.config.idType === 'udi' ? i.udi : i.id; + }); + $scope.model.value = trim(currIds.join(), ','); + }); + } + function trim(str, chr) { + var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); + return str.replace(rgxtrim, ''); + } + function startWatch() { + //We need to watch our renderModel so that we can update the underlying $scope.model.value properly, this is required + // because the ui-sortable doesn't dispatch an event after the digest of the sort operation. Any of the events for UI sortable + // occur after the DOM has updated but BEFORE the digest has occured so the model has NOT changed yet - it even states so in the docs. + // In their source code there is no event so we need to just subscribe to our model changes here. + //This also makes it easier to manage models, we update one and the rest will just work. + $scope.$watch(function () { + //return the joined Ids as a string to watch + return _.map($scope.renderModel, function (i) { + return $scope.model.config.idType === 'udi' ? i.udi : i.id; + }).join(); + }, function (newVal) { + var currIds = _.map($scope.renderModel, function (i) { + return $scope.model.config.idType === 'udi' ? i.udi : i.id; + }); + $scope.model.value = trim(currIds.join(), ','); + //Validate! + if ($scope.model.config && $scope.model.config.minNumber && parseInt($scope.model.config.minNumber) > $scope.renderModel.length) { + $scope.contentPickerForm.minCount.$setValidity('minCount', false); + } else { + $scope.contentPickerForm.minCount.$setValidity('minCount', true); + } + if ($scope.model.config && $scope.model.config.maxNumber && parseInt($scope.model.config.maxNumber) < $scope.renderModel.length) { + $scope.contentPickerForm.maxCount.$setValidity('maxCount', false); + } else { + $scope.contentPickerForm.maxCount.$setValidity('maxCount', true); + } + setSortingState($scope.renderModel); + }); + } + $scope.renderModel = []; + $scope.dialogEditor = editorState && editorState.current && editorState.current.isDialogEditor === true; + //the default pre-values + var defaultConfig = { + multiPicker: false, + showOpenButton: false, + showEditButton: false, + showPathOnHover: false, + startNode: { + query: '', + type: 'content', + id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker + } + }; + // sortable options + $scope.sortableOptions = { + distance: 10, + tolerance: 'pointer', + scroll: true, + zIndex: 6000 + }; + if ($scope.model.config) { + //merge the server config on top of the default config, then set the server config to use the result + $scope.model.config = angular.extend(defaultConfig, $scope.model.config); + } + //Umbraco persists boolean for prevalues as "0" or "1" so we need to convert that! + $scope.model.config.multiPicker = $scope.model.config.multiPicker === '1' ? true : false; + $scope.model.config.showOpenButton = $scope.model.config.showOpenButton === '1' ? true : false; + $scope.model.config.showEditButton = $scope.model.config.showEditButton === '1' ? true : false; + $scope.model.config.showPathOnHover = $scope.model.config.showPathOnHover === '1' ? true : false; + var entityType = $scope.model.config.startNode.type === 'member' ? 'Member' : $scope.model.config.startNode.type === 'media' ? 'Media' : 'Document'; + $scope.allowOpenButton = entityType === 'Document'; + $scope.allowEditButton = entityType === 'Document'; + $scope.allowRemoveButton = true; + //the dialog options for the picker + var dialogOptions = { + multiPicker: $scope.model.config.multiPicker, + entityType: entityType, + filterCssClass: 'not-allowed not-published', + startNodeId: null, + callback: function (data) { + if (angular.isArray(data)) { + _.each(data, function (item, i) { + $scope.add(item); + }); + } else { + $scope.clear(); + $scope.add(data); + } + angularHelper.getCurrentForm($scope).$setDirty(); + }, + treeAlias: $scope.model.config.startNode.type, + section: $scope.model.config.startNode.type, + idType: 'int' + }; + //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the + // pre-value config on to the dialog options + angular.extend(dialogOptions, $scope.model.config); + //We need to manually handle the filter for members here since the tree displayed is different and only contains + // searchable list views + if (entityType === 'Member') { + //first change the not allowed filter css class + dialogOptions.filterCssClass = 'not-allowed'; + var currFilter = dialogOptions.filter; + //now change the filter to be a method + dialogOptions.filter = function (i) { + //filter out the list view nodes + if (i.metaData.isContainer) { + return true; + } + if (!currFilter) { + return false; + } + //now we need to filter based on what is stored in the pre-vals, this logic duplicates what is in the treepicker.controller, + // but not much we can do about that since members require special filtering. + var filterItem = currFilter.toLowerCase().split(','); + var found = filterItem.indexOf(i.metaData.contentType.toLowerCase()) >= 0; + if (!currFilter.startsWith('!') && !found || currFilter.startsWith('!') && found) { + return true; + } + return false; + }; + } + if ($routeParams.section === 'settings' && $routeParams.tree === 'documentTypes') { + //if the content-picker is being rendered inside the document-type editor, we don't need to process the startnode query + dialogOptions.startNodeId = -1; + } else if ($scope.model.config.startNode.query) { + //if we have a query for the startnode, we will use that. + var rootId = $routeParams.id; + entityResource.getByQuery($scope.model.config.startNode.query, rootId, 'Document').then(function (ent) { + dialogOptions.startNodeId = $scope.model.config.idType === 'udi' ? ent.udi : ent.id; + }); + } else { + dialogOptions.startNodeId = $scope.model.config.startNode.id; + } + //dialog + $scope.openContentPicker = function () { + $scope.contentPickerOverlay = dialogOptions; + $scope.contentPickerOverlay.view = 'treepicker'; + $scope.contentPickerOverlay.show = true; + $scope.contentPickerOverlay.submit = function (model) { + if (angular.isArray(model.selection)) { + _.each(model.selection, function (item, i) { + $scope.add(item); + }); + } + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + $scope.contentPickerOverlay.close = function (oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + }; + $scope.remove = function (index) { + $scope.renderModel.splice(index, 1); + angularHelper.getCurrentForm($scope).$setDirty(); + }; + $scope.showNode = function (index) { + var item = $scope.renderModel[index]; + var id = item.id; + var section = $scope.model.config.startNode.type.toLowerCase(); + entityResource.getPath(id, entityType).then(function (path) { + navigationService.changeSection(section); + navigationService.showTree(section, { + tree: section, + path: path, + forceReload: false, + activate: true + }); + var routePath = section + '/' + section + '/edit/' + id.toString(); + $location.path(routePath).search(''); + }); + }; + $scope.add = function (item) { + var currIds = _.map($scope.renderModel, function (i) { + return $scope.model.config.idType === 'udi' ? i.udi : i.id; + }); + var itemId = $scope.model.config.idType === 'udi' ? item.udi : item.id; + if (currIds.indexOf(itemId) < 0) { + setEntityUrl(item); + } + }; + $scope.clear = function () { + $scope.renderModel = []; + }; + $scope.openMiniEditor = function (node) { + miniEditorHelper.launchMiniEditor(node).then(function (updatedNode) { + // update the node + node.name = updatedNode.name; + node.published = updatedNode.hasPublishedVersion; + if (entityType !== 'Member') { + entityResource.getUrl(updatedNode.id, entityType).then(function (data) { + node.url = data; + }); + } + }); + }; + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + if (unsubscribe) { + unsubscribe(); + } + }); + var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; + //load current data if anything selected + if (modelIds.length > 0) { + entityResource.getByIds(modelIds, entityType).then(function (data) { + _.each(modelIds, function (id, i) { + var entity = _.find(data, function (d) { + return $scope.model.config.idType === 'udi' ? d.udi == id : d.id == id; + }); + if (entity) { + setEntityUrl(entity); + } + }); + //everything is loaded, start the watch on the model + startWatch(); + subscribe(); + }); + } else { + //everything is loaded, start the watch on the model + startWatch(); + subscribe(); + } + function setEntityUrl(entity) { + // get url for content and media items + if (entityType !== 'Member') { + entityResource.getUrl(entity.id, entityType).then(function (data) { + // update url + angular.forEach($scope.renderModel, function (item) { + if (item.id === entity.id) { + if (entity.trashed) { + item.url = localizationService.dictionary.general_recycleBin; + } else { + item.url = data; + } + } + }); + }); + } + // add the selected item to the renderModel + // if it needs to show a url the item will get + // updated when the url comes back from server + addSelectedItem(entity); + } + function addSelectedItem(item) { + // set icon + if (item.icon) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + } + // set default icon + if (!item.icon) { + switch (entityType) { + case 'Document': + item.icon = 'icon-document'; + break; + case 'Media': + item.icon = 'icon-picture'; + break; + case 'Member': + item.icon = 'icon-user'; + break; + } + } + $scope.renderModel.push({ + 'name': item.name, + 'id': item.id, + 'udi': item.udi, + 'icon': item.icon, + 'path': item.path, + 'url': item.url, + 'trashed': item.trashed, + 'published': item.metaData && item.metaData.IsPublished === false && entityType === 'Document' ? false : true // only content supports published/unpublished content so we set everything else to published so the UI looks correct + }); + } + function setSortingState(items) { + // disable sorting if the list only consist of one item + if (items.length > 1) { + $scope.sortableOptions.disabled = false; + } else { + $scope.sortableOptions.disabled = true; + } + } + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.ContentPickerController', contentPickerController); + function dateTimePickerController($scope, notificationsService, assetsService, angularHelper, userService, $element, dateHelper) { + //setup the default config + var config = { + pickDate: true, + pickTime: true, + useSeconds: true, + format: 'YYYY-MM-DD HH:mm:ss', + icons: { + time: 'icon-time', + date: 'icon-calendar', + up: 'icon-chevron-up', + down: 'icon-chevron-down' + } + }; + //map the user config + $scope.model.config = angular.extend(config, $scope.model.config); + //ensure the format doesn't get overwritten with an empty string + if ($scope.model.config.format === '' || $scope.model.config.format === undefined || $scope.model.config.format === null) { + $scope.model.config.format = $scope.model.config.pickTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; + } + $scope.hasDatetimePickerValue = $scope.model.value ? true : false; + $scope.datetimePickerValue = null; + //hide picker if clicking on the document + $scope.hidePicker = function () { + //$element.find("div:first").datetimepicker("hide"); + // Sometimes the statement above fails and generates errors in the browser console. The following statements fix that. + var dtp = $element.find('div:first'); + if (dtp && dtp.datetimepicker) { + dtp.datetimepicker('hide'); + } + }; + $(document).bind('click', $scope.hidePicker); + //handles the date changing via the api + function applyDate(e) { + angularHelper.safeApply($scope, function () { + // when a date is changed, update the model + if (e.date && e.date.isValid()) { + $scope.datePickerForm.datepicker.$setValidity('pickerError', true); + $scope.hasDatetimePickerValue = true; + $scope.datetimePickerValue = e.date.format($scope.model.config.format); + } else { + $scope.hasDatetimePickerValue = false; + $scope.datetimePickerValue = null; + } + setModelValue(); + if (!$scope.model.config.pickTime) { + $element.find('div:first').datetimepicker('hide', 0); + } + }); + } + //sets the scope model value accordingly - this is the value to be sent up to the server and depends on + // if the picker is configured to offset time. We always format the date/time in a specific format for sending + // to the server, this is different from the format used to display the date/time. + function setModelValue() { + if ($scope.hasDatetimePickerValue) { + var elementData = $element.find('div:first').data().DateTimePicker; + if ($scope.model.config.pickTime) { + //check if we are supposed to offset the time + if ($scope.model.value && $scope.model.config.offsetTime === '1' && Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { + $scope.model.value = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset); + $scope.serverTime = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset, 'YYYY-MM-DD HH:mm:ss Z'); + } else { + $scope.model.value = elementData.getDate().format('YYYY-MM-DD HH:mm:ss'); + } + } else { + $scope.model.value = elementData.getDate().format('YYYY-MM-DD'); + } + } else { + $scope.model.value = null; + } + } + var picker = null; + $scope.clearDate = function () { + $scope.hasDatetimePickerValue = false; + $scope.datetimePickerValue = null; + $scope.model.value = null; + $scope.datePickerForm.datepicker.$setValidity('pickerError', true); + }; + $scope.serverTime = null; + $scope.serverTimeNeedsOffsetting = false; + if (Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { + // Will return something like 120 + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + // Will return something like -120 + var localOffset = new Date().getTimezoneOffset(); + // If these aren't equal then offsetting is needed + // note the minus in front of serverOffset needed + // because C# and javascript return the inverse offset + $scope.serverTimeNeedsOffsetting = -serverOffset !== localOffset; + } + //get the current user to see if we can localize this picker + userService.getCurrentUser().then(function (user) { + assetsService.loadCss('lib/datetimepicker/bootstrap-datetimepicker.min.css').then(function () { + var filesToLoad = [ + 'lib/moment/moment-with-locales.js', + 'lib/datetimepicker/bootstrap-datetimepicker.js' + ]; + $scope.model.config.language = user.locale; + assetsService.load(filesToLoad, $scope).then(function () { + //The Datepicker js and css files are available and all components are ready to use. + // Get the id of the datepicker button that was clicked + var pickerId = $scope.model.alias; + var element = $element.find('div:first'); + // Open the datepicker and add a changeDate eventlistener + element.datetimepicker(angular.extend({ useCurrent: true }, $scope.model.config)).on('dp.change', applyDate).on('dp.error', function (a, b, c) { + $scope.hasDatetimePickerValue = false; + $scope.datePickerForm.datepicker.$setValidity('pickerError', false); + }); + if ($scope.hasDatetimePickerValue) { + var dateVal; + //check if we are supposed to offset the time + if ($scope.model.value && $scope.model.config.offsetTime === '1' && $scope.serverTimeNeedsOffsetting) { + //get the local time offset from the server + dateVal = dateHelper.convertToLocalMomentTime($scope.model.value, Umbraco.Sys.ServerVariables.application.serverTimeOffset); + $scope.serverTime = dateHelper.convertToServerStringTime(dateVal, Umbraco.Sys.ServerVariables.application.serverTimeOffset, 'YYYY-MM-DD HH:mm:ss Z'); + } else { + //create a normal moment , no offset required + var dateVal = $scope.model.value ? moment($scope.model.value, 'YYYY-MM-DD HH:mm:ss') : moment(); + } + element.datetimepicker('setValue', dateVal); + $scope.datetimePickerValue = dateVal.format($scope.model.config.format); + } + element.find('input').bind('blur', function () { + //we need to force an apply here + $scope.$apply(); + }); + //Ensure to remove the event handler when this instance is destroyted + $scope.$on('$destroy', function () { + element.find('input').unbind('blur'); + element.datetimepicker('destroy'); + }); + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + setModelValue(); + }); + //unbind doc click event! + $scope.$on('$destroy', function () { + unsubscribe(); + }); + }); + }); + }); + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + setModelValue(); + }); + //unbind doc click event! + $scope.$on('$destroy', function () { + $(document).unbind('click', $scope.hidePicker); + unsubscribe(); + }); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.DatepickerController', dateTimePickerController); + angular.module('umbraco').controller('Umbraco.PropertyEditors.DropdownController', function ($scope) { + //setup the default config + var config = { + items: [], + multiple: false + }; + //map the user config + angular.extend(config, $scope.model.config); + //map back to the model + $scope.model.config = config; + function convertArrayToDictionaryArray(model) { + //now we need to format the items in the dictionary because we always want to have an array + var newItems = []; + for (var i = 0; i < model.length; i++) { + newItems.push({ + id: model[i], + sortOrder: 0, + value: model[i] + }); + } + return newItems; + } + function convertObjectToDictionaryArray(model) { + //now we need to format the items in the dictionary because we always want to have an array + var newItems = []; + var vals = _.values($scope.model.config.items); + var keys = _.keys($scope.model.config.items); + for (var i = 0; i < vals.length; i++) { + var label = vals[i].value ? vals[i].value : vals[i]; + newItems.push({ + id: keys[i], + sortOrder: vals[i].sortOrder, + value: label + }); + } + return newItems; + } + if (angular.isArray($scope.model.config.items)) { + //PP: I dont think this will happen, but we have tests that expect it to happen.. + //if array is simple values, convert to array of objects + if (!angular.isObject($scope.model.config.items[0])) { + $scope.model.config.items = convertArrayToDictionaryArray($scope.model.config.items); + } + } else if (angular.isObject($scope.model.config.items)) { + $scope.model.config.items = convertObjectToDictionaryArray($scope.model.config.items); + } else { + throw 'The items property must be either an array or a dictionary'; + } + //sort the values + $scope.model.config.items.sort(function (a, b) { + return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; + }); + //now we need to check if the value is null/undefined, if it is we need to set it to "" so that any value that is set + // to "" gets selected by default + if ($scope.model.value === null || $scope.model.value === undefined) { + if ($scope.model.config.multiple) { + $scope.model.value = []; + } else { + $scope.model.value = ''; + } + } + }); + /** A drop down list or multi value select list based on an entity type, this can be re-used for any entity types */ + function entityPicker($scope, entityResource) { + //set the default to DocumentType + if (!$scope.model.config.entityType) { + $scope.model.config.entityType = 'DocumentType'; + } + //Determine the select list options and which value to publish + if (!$scope.model.config.publishBy) { + $scope.selectOptions = 'entity.id as entity.name for entity in entities'; + } else { + $scope.selectOptions = 'entity.' + $scope.model.config.publishBy + ' as entity.name for entity in entities'; + } + entityResource.getAll($scope.model.config.entityType).then(function (data) { + //convert the ids to strings so the drop downs work properly when comparing + _.each(data, function (d) { + d.id = d.id.toString(); + }); + $scope.entities = data; + }); + if ($scope.model.value === null || $scope.model.value === undefined) { + if ($scope.model.config.multiple) { + $scope.model.value = []; + } else { + $scope.model.value = ''; + } + } else { + //if it's multiple, change the value to an array + if ($scope.model.config.multiple === '1') { + if (_.isString($scope.model.value)) { + $scope.model.value = $scope.model.value.split(','); + } + } + } + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.EntityPickerController', entityPicker); + /** + * @ngdoc controller + * @name Umbraco.Editors.FileUploadController + * @function + * + * @description + * The controller for the file upload property editor. It is important to note that the $scope.model.value + * doesn't necessarily depict what is saved for this property editor. $scope.model.value can be empty when we + * are submitting files because in that case, we are adding files to the fileManager which is what gets peristed + * on the server. However, when we are clearing files, we are setting $scope.model.value to "{clearFiles: true}" + * to indicate on the server that we are removing files for this property. We will keep the $scope.model.value to + * be the name of the file selected (if it is a newly selected file) or keep it to be it's original value, this allows + * for the editors to check if the value has changed and to re-bind the property if that is true. + * +*/ + function fileUploadController($scope, $element, $compile, imageHelper, fileManager, umbRequestHelper, mediaHelper) { + /** Clears the file collections when content is saving (if we need to clear) or after saved */ + function clearFiles() { + //clear the files collection (we don't want to upload any!) + fileManager.setFiles($scope.model.alias, []); + //clear the current files + $scope.files = []; + if ($scope.propertyForm) { + if ($scope.propertyForm.fileCount) { + //this is required to re-validate + $scope.propertyForm.fileCount.$setViewValue($scope.files.length); + } + } + } + /** this method is used to initialize the data and to re-initialize it if the server value is changed */ + function initialize(index) { + clearFiles(); + if (!index) { + index = 1; + } + //this is used in order to tell the umb-single-file-upload directive to + //rebuild the html input control (and thus clearing the selected file) since + //that is the only way to manipulate the html for the file input control. + $scope.rebuildInput = { index: index }; + //clear the current files + $scope.files = []; + //store the original value so we can restore it if the user clears and then cancels clearing. + $scope.originalValue = $scope.model.value; + //create the property to show the list of files currently saved + if ($scope.model.value != '' && $scope.model.value != undefined) { + var images = $scope.model.value.split(','); + $scope.persistedFiles = _.map(images, function (item) { + return { + file: item, + isImage: imageHelper.detectIfImageByExtension(item) + }; + }); + } else { + $scope.persistedFiles = []; + } + _.each($scope.persistedFiles, function (file) { + var thumbnailUrl = umbRequestHelper.getApiUrl('imagesApiBaseUrl', 'GetBigThumbnail', [{ originalImagePath: file.file }]); + var extension = file.file.substring(file.file.lastIndexOf('.') + 1, file.file.length); + file.thumbnail = thumbnailUrl; + file.extension = extension.toLowerCase(); + }); + $scope.clearFiles = false; + } + initialize(); + // Method required by the valPropertyValidator directive (returns true if the property editor has at least one file selected) + $scope.validateMandatory = function () { + return { + isValid: !$scope.model.validation.mandatory || ($scope.persistedFiles != null && $scope.persistedFiles.length > 0 || $scope.files != null && $scope.files.length > 0) && !$scope.clearFiles, + errorMsg: 'Value cannot be empty', + errorKey: 'required' + }; + }; + //listen for clear files changes to set our model to be sent up to the server + $scope.$watch('clearFiles', function (isCleared) { + if (isCleared == true) { + $scope.model.value = { clearFiles: true }; + clearFiles(); + } else { + //reset to original value + $scope.model.value = $scope.originalValue; + //this is required to re-validate + if ($scope.propertyForm) { + $scope.propertyForm.fileCount.$setViewValue($scope.files.length); + } + } + }); + //listen for when a file is selected + $scope.$on('filesSelected', function (event, args) { + $scope.$apply(function () { + //set the files collection + fileManager.setFiles($scope.model.alias, args.files); + //clear the current files + $scope.files = []; + var newVal = ''; + for (var i = 0; i < args.files.length; i++) { + //save the file object to the scope's files collection + $scope.files.push({ + alias: $scope.model.alias, + file: args.files[i] + }); + newVal += args.files[i].name + ','; + } + //this is required to re-validate + $scope.propertyForm.fileCount.$setViewValue($scope.files.length); + //set clear files to false, this will reset the model too + $scope.clearFiles = false; + //set the model value to be the concatenation of files selected. Please see the notes + // in the description of this controller, it states that this value isn't actually used for persistence, + // but we need to set it so that the editor and the server can detect that it's been changed, and it is used for validation. + $scope.model.value = { selectedFiles: newVal.trimEnd(',') }; + }); + }); + //listen for when the model value has changed + $scope.$watch('model.value', function (newVal, oldVal) { + //cannot just check for !newVal because it might be an empty string which we + //want to look for. + if (newVal !== null && newVal !== undefined && newVal !== oldVal) { + // here we need to check if the value change needs to trigger an update in the UI. + // if the value is only changed in the controller and not in the server values, we do not + // want to trigger an update yet. + // we can however no longer rely on checking values in the controller vs. values from the server + // to determine whether to update or not, since you could potentially be uploading a file with + // the exact same name - in that case we need to reinitialize to show the newly uploaded file. + if (newVal.clearFiles !== true && !newVal.selectedFiles) { + initialize($scope.rebuildInput.index + 1); + } + } + }); + } + ; + angular.module('umbraco').controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController).run(function (mediaHelper, umbRequestHelper, assetsService) { + if (mediaHelper && mediaHelper.registerFileResolver) { + assetsService.load(['lib/moment/moment-with-locales.js']).then(function () { + //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource + // they contain different data structures so if we need to query against it we need to be aware of this. + mediaHelper.registerFileResolver('Umbraco.UploadField', function (property, entity, thumbnail) { + if (thumbnail) { + if (mediaHelper.detectIfImageByExtension(property.value)) { + //get default big thumbnail from image processor + var thumbnailUrl = property.value + '?rnd=' + moment(entity.updateDate).format('YYYYMMDDHHmmss') + '&width=500&animationprocessmode=first'; + return thumbnailUrl; + } else { + return null; + } + } else { + return property.value; + } + }); + }); + } + }); + angular.module('umbraco') //this controller is obsolete and should not be used anymore + //it proxies everything to the system media list view which has overtaken + //all the work this property editor used to perform +.controller('Umbraco.PropertyEditors.FolderBrowserController', function ($rootScope, $scope, contentTypeResource) { + //get the system media listview + contentTypeResource.getPropertyTypeScaffold(-96).then(function (dt) { + $scope.fakeProperty = { + alias: 'contents', + config: dt.config, + description: '', + editor: dt.editor, + hideLabel: true, + id: 1, + label: 'Contents:', + validation: { + mandatory: false, + pattern: null + }, + value: '', + view: dt.view + }; + }); + }); + angular.module('umbraco').controller('Umbraco.PropertyEditors.GoogleMapsController', function ($element, $rootScope, $scope, notificationsService, dialogService, assetsService, $log, $timeout) { + assetsService.loadJs('https://www.google.com/jsapi').then(function () { + google.load('maps', '3', { + callback: initMap, + other_params: 'sensor=false' + }); + }); + function initMap() { + //Google maps is available and all components are ready to use. + var valueArray = $scope.model.value.split(','); + var latLng = new google.maps.LatLng(valueArray[0], valueArray[1]); + var mapDiv = document.getElementById($scope.model.alias + '_map'); + var mapOptions = { + zoom: $scope.model.config.zoom, + center: latLng, + mapTypeId: google.maps.MapTypeId[$scope.model.config.mapType] + }; + var geocoder = new google.maps.Geocoder(); + var map = new google.maps.Map(mapDiv, mapOptions); + var marker = new google.maps.Marker({ + map: map, + position: latLng, + draggable: true + }); + google.maps.event.addListener(map, 'click', function (event) { + dialogService.mediaPicker({ + callback: function (data) { + var image = data.selection[0].src; + var latLng = event.latLng; + var marker = new google.maps.Marker({ + map: map, + icon: image, + position: latLng, + draggable: true + }); + google.maps.event.addListener(marker, 'dragend', function (e) { + var newLat = marker.getPosition().lat(); + var newLng = marker.getPosition().lng(); + codeLatLng(marker.getPosition(), geocoder); + //set the model value + $scope.model.vvalue = newLat + ',' + newLng; + }); + } + }); + }); + var tabShown = function (e) { + google.maps.event.trigger(map, 'resize'); + }; + //listen for tab changes + if (tabsCtrl != null) { + tabsCtrl.onTabShown(function (args) { + tabShown(); + }); + } + $element.closest('.umb-panel.tabbable').on('shown', '.nav-tabs a', tabShown); + $scope.$on('$destroy', function () { + $element.closest('.umb-panel.tabbable').off('shown', '.nav-tabs a', tabShown); + }); + } + function codeLatLng(latLng, geocoder) { + geocoder.geocode({ 'latLng': latLng }, function (results, status) { + if (status == google.maps.GeocoderStatus.OK) { + var location = results[0].formatted_address; + $rootScope.$apply(function () { + notificationsService.success('Peter just went to: ', location); + }); + } + }); + } + //here we declare a special method which will be called whenever the value has changed from the server + //this is instead of doing a watch on the model.value = faster + $scope.model.onValueChanged = function (newVal, oldVal) { + //update the display val again if it has changed from the server + initMap(); + }; + }); + angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditor.LayoutConfigController', function ($scope) { + $scope.currentLayout = $scope.model.currentLayout; + $scope.columns = $scope.model.columns; + $scope.rows = $scope.model.rows; + $scope.scaleUp = function (section, max, overflow) { + var add = 1; + if (overflow !== true) { + add = max > 1 ? 1 : max; + } + //var add = (max > 1) ? 1 : max; + section.grid = section.grid + add; + }; + $scope.scaleDown = function (section) { + var remove = section.grid > 1 ? 1 : 0; + section.grid = section.grid - remove; + }; + $scope.percentage = function (spans) { + return (spans / $scope.columns * 100).toFixed(8); + }; + $scope.toggleCollection = function (collection, toggle) { + if (toggle) { + collection = []; + } else { + delete collection; + } + }; + /**************** + Section + *****************/ + $scope.configureSection = function (section, template) { + if (section === undefined) { + var space = $scope.availableLayoutSpace > 4 ? 4 : $scope.availableLayoutSpace; + section = { grid: space }; + template.sections.push(section); + } + $scope.currentSection = section; + }; + $scope.deleteSection = function (section, template) { + if ($scope.currentSection === section) { + $scope.currentSection = undefined; + } + var index = template.sections.indexOf(section); + template.sections.splice(index, 1); + }; + $scope.closeSection = function () { + $scope.currentSection = undefined; + }; + $scope.$watch('currentLayout', function (layout) { + if (layout) { + var total = 0; + _.forEach(layout.sections, function (section) { + total = total + section.grid; + }); + $scope.availableLayoutSpace = $scope.columns - total; + } + }, true); + }); + function RowConfigController($scope) { + $scope.currentRow = $scope.model.currentRow; + $scope.editors = $scope.model.editors; + $scope.columns = $scope.model.columns; + $scope.scaleUp = function (section, max, overflow) { + var add = 1; + if (overflow !== true) { + add = max > 1 ? 1 : max; + } + //var add = (max > 1) ? 1 : max; + section.grid = section.grid + add; + }; + $scope.scaleDown = function (section) { + var remove = section.grid > 1 ? 1 : 0; + section.grid = section.grid - remove; + }; + $scope.percentage = function (spans) { + return (spans / $scope.columns * 100).toFixed(8); + }; + $scope.toggleCollection = function (collection, toggle) { + if (toggle) { + collection = []; + } else { + delete collection; + } + }; + /**************** + area + *****************/ + $scope.configureCell = function (cell, row) { + if ($scope.currentCell && $scope.currentCell === cell) { + delete $scope.currentCell; + } else { + if (cell === undefined) { + var available = $scope.availableRowSpace; + var space = 4; + if (available < 4 && available > 0) { + space = available; + } + cell = { grid: space }; + row.areas.push(cell); + } + $scope.currentCell = cell; + } + }; + $scope.deleteArea = function (cell, row) { + if ($scope.currentCell === cell) { + $scope.currentCell = undefined; + } + var index = row.areas.indexOf(cell); + row.areas.splice(index, 1); + }; + $scope.closeArea = function () { + $scope.currentCell = undefined; + }; + $scope.nameChanged = false; + var originalName = $scope.currentRow.name; + $scope.$watch('currentRow', function (row) { + if (row) { + var total = 0; + _.forEach(row.areas, function (area) { + total = total + area.grid; + }); + $scope.availableRowSpace = $scope.columns - total; + if (originalName) { + if (originalName != row.name) { + $scope.nameChanged = true; + } else { + $scope.nameChanged = false; + } + } + } + }, true); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditor.RowConfigController', RowConfigController); + angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.EmbedController', function ($scope, $rootScope, $timeout) { + $scope.setEmbed = function () { + $scope.embedDialog = {}; + $scope.embedDialog.view = 'embed'; + $scope.embedDialog.show = true; + $scope.embedDialog.submit = function (model) { + $scope.control.value = model.embed.preview; + $scope.embedDialog.show = false; + $scope.embedDialog = null; + }; + $scope.embedDialog.close = function (oldModel) { + $scope.embedDialog.show = false; + $scope.embedDialog = null; + }; + }; + $timeout(function () { + if ($scope.control.$initializing) { + $scope.setEmbed(); + } + }, 200); + }); + angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.MacroController', function ($scope, $rootScope, $timeout, dialogService, macroResource, macroService, $routeParams) { + $scope.title = 'Click to insert macro'; + $scope.setMacro = function () { + var dialogData = { + richTextEditor: true, + macroData: $scope.control.value || { macroAlias: $scope.control.editor.config && $scope.control.editor.config.macroAlias ? $scope.control.editor.config.macroAlias : '' } + }; + $scope.macroPickerOverlay = {}; + $scope.macroPickerOverlay.view = 'macropicker'; + $scope.macroPickerOverlay.dialogData = dialogData; + $scope.macroPickerOverlay.show = true; + $scope.macroPickerOverlay.submit = function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + $scope.control.value = { + macroAlias: macroObject.macroAlias, + macroParamsDictionary: macroObject.macroParamsDictionary + }; + $scope.setPreview($scope.control.value); + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + $scope.macroPickerOverlay.close = function (oldModel) { + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + }; + $scope.setPreview = function (macro) { + var contentId = $routeParams.id; + macroResource.getMacroResultAsHtmlForEditor(macro.macroAlias, contentId, macro.macroParamsDictionary).then(function (htmlResult) { + $scope.title = macro.macroAlias; + if (htmlResult.trim().length > 0 && htmlResult.indexOf('Macro:') < 0) { + $scope.preview = htmlResult; + } + }); + }; + $timeout(function () { + if ($scope.control.$initializing) { + $scope.setMacro(); + } else if ($scope.control.value) { + $scope.setPreview($scope.control.value); + } + }, 200); + }); + angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.MediaController', function ($scope, $rootScope, $timeout, userService) { + if (!$scope.model.config.startNodeId) { + userService.getCurrentUser().then(function (userData) { + $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; + }); + } + $scope.setImage = function () { + $scope.mediaPickerOverlay = {}; + $scope.mediaPickerOverlay.view = 'mediapicker'; + $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; + $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; + $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; + $scope.mediaPickerOverlay.showDetails = true; + $scope.mediaPickerOverlay.disableFolderSelect = true; + $scope.mediaPickerOverlay.onlyImages = true; + $scope.mediaPickerOverlay.show = true; + $scope.mediaPickerOverlay.submit = function (model) { + var selectedImage = model.selectedImages[0]; + $scope.control.value = { + focalPoint: selectedImage.focalPoint, + id: selectedImage.id, + udi: selectedImage.udi, + image: selectedImage.image, + altText: selectedImage.altText + }; + $scope.setUrl(); + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; + $scope.mediaPickerOverlay.close = function (oldModel) { + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; + }; + $scope.setUrl = function () { + if ($scope.control.value.image) { + var url = $scope.control.value.image; + if ($scope.control.editor.config && $scope.control.editor.config.size) { + url += '?width=' + $scope.control.editor.config.size.width; + url += '&height=' + $scope.control.editor.config.size.height; + url += '&animationprocessmode=first'; + if ($scope.control.value.focalPoint) { + url += '¢er=' + $scope.control.value.focalPoint.top + ',' + $scope.control.value.focalPoint.left; + url += '&mode=crop'; + } + } + // set default size if no crop present (moved from the view) + if (url.indexOf('?') == -1) { + url += '?width=800&upscale=false&animationprocessmode=false'; + } + $scope.url = url; + } + }; + $timeout(function () { + if ($scope.control.$initializing) { + $scope.setImage(); + } else if ($scope.control.value) { + $scope.setUrl(); + } + }, 200); + }); + (function () { + 'use strict'; + function GridRichTextEditorController($scope, tinyMceService, macroService) { + var vm = this; + vm.openLinkPicker = openLinkPicker; + vm.openMediaPicker = openMediaPicker; + vm.openMacroPicker = openMacroPicker; + vm.openEmbed = openEmbed; + function openLinkPicker(editor, currentTarget, anchorElement) { + vm.linkPickerOverlay = { + view: 'linkpicker', + currentTarget: currentTarget, + show: true, + submit: function (model) { + tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); + vm.linkPickerOverlay.show = false; + vm.linkPickerOverlay = null; + } + }; + } + function openMediaPicker(editor, currentTarget, userData) { + vm.mediaPickerOverlay = { + currentTarget: currentTarget, + onlyImages: true, + showDetails: true, + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], + view: 'mediapicker', + show: true, + submit: function (model) { + tinyMceService.insertMediaInEditor(editor, model.selectedImages[0]); + vm.mediaPickerOverlay.show = false; + vm.mediaPickerOverlay = null; + } + }; + } + function openEmbed(editor) { + vm.embedOverlay = { + view: 'embed', + show: true, + submit: function (model) { + tinyMceService.insertEmbeddedMediaInEditor(editor, model.embed.preview); + vm.embedOverlay.show = false; + vm.embedOverlay = null; + } + }; + } + function openMacroPicker(editor, dialogData) { + vm.macroPickerOverlay = { + view: 'macropicker', + dialogData: dialogData, + show: true, + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + tinyMceService.insertMacroInEditor(editor, macroObject, $scope); + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + } + }; + } + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.RichTextEditorController', GridRichTextEditorController); + }()); + angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.TextStringController', function ($scope, $rootScope, $timeout, dialogService) { + }); + angular.module('umbraco').controller('Umbraco.PropertyEditors.GridController', function ($scope, $http, assetsService, localizationService, $rootScope, dialogService, gridService, mediaResource, imageHelper, $timeout, umbRequestHelper, angularHelper) { + // Grid status variables + var placeHolder = ''; + var currentForm = angularHelper.getCurrentForm($scope); + $scope.currentRow = null; + $scope.currentCell = null; + $scope.currentToolsControl = null; + $scope.currentControl = null; + $scope.openRTEToolbarId = null; + $scope.hasSettings = false; + $scope.showRowConfigurations = true; + $scope.sortMode = false; + $scope.reorderKey = 'general_reorder'; + // ********************************************* + // Sortable options + // ********************************************* + var draggedRteSettings; + $scope.sortableOptionsRow = { + distance: 10, + cursor: 'move', + placeholder: 'ui-sortable-placeholder', + handle: '.umb-row-title-bar', + helper: 'clone', + forcePlaceholderSize: true, + tolerance: 'pointer', + zIndex: 1000000000000000000, + scrollSensitivity: 100, + cursorAt: { + top: 40, + left: 60 + }, + sort: function (event, ui) { + /* prevent vertical scroll out of the screen */ + var max = $('.umb-grid').width() - 150; + if (parseInt(ui.helper.css('left')) > max) { + ui.helper.css({ 'left': max + 'px' }); + } + if (parseInt(ui.helper.css('left')) < 20) { + ui.helper.css({ 'left': 20 }); + } + }, + start: function (e, ui) { + // Fade out row when sorting + ui.item.context.style.display = 'block'; + ui.item.context.style.opacity = '0.5'; + draggedRteSettings = {}; + ui.item.find('.mceNoEditor').each(function () { + // remove all RTEs in the dragged row and save their settings + var id = $(this).attr('id'); + draggedRteSettings[id] = _.findWhere(tinyMCE.editors, { id: id }).settings; // tinyMCE.execCommand("mceRemoveEditor", false, id); + }); + }, + stop: function (e, ui) { + // Fade in row when sorting stops + ui.item.context.style.opacity = '1'; + // reset all RTEs affected by the dragging + ui.item.parents('.umb-column').find('.mceNoEditor').each(function () { + var id = $(this).attr('id'); + draggedRteSettings[id] = draggedRteSettings[id] || _.findWhere(tinyMCE.editors, { id: id }).settings; + tinyMCE.execCommand('mceRemoveEditor', false, id); + tinyMCE.init(draggedRteSettings[id]); + }); + currentForm.$setDirty(); + } + }; + var notIncludedRte = []; + var cancelMove = false; + var startingArea; + $scope.sortableOptionsCell = { + distance: 10, + cursor: 'move', + placeholder: 'ui-sortable-placeholder', + handle: '.umb-control-handle', + helper: 'clone', + connectWith: '.umb-cell-inner', + forcePlaceholderSize: true, + tolerance: 'pointer', + zIndex: 1000000000000000000, + scrollSensitivity: 100, + cursorAt: { + top: 45, + left: 90 + }, + sort: function (event, ui) { + /* prevent vertical scroll out of the screen */ + var position = parseInt(ui.item.parent().offset().left) + parseInt(ui.helper.css('left')) - parseInt($('.umb-grid').offset().left); + var max = $('.umb-grid').width() - 220; + if (position > max) { + ui.helper.css({ 'left': max - parseInt(ui.item.parent().offset().left) + parseInt($('.umb-grid').offset().left) + 'px' }); + } + if (position < 0) { + ui.helper.css({ 'left': 0 - parseInt(ui.item.parent().offset().left) + parseInt($('.umb-grid').offset().left) + 'px' }); + } + }, + over: function (event, ui) { + var area = $(event.target).scope().area; + var allowedEditors = area.allowed; + if ($.inArray(ui.item.scope().control.editor.alias, allowedEditors) < 0 && allowedEditors || startingArea != area && area.maxItems != '' && area.maxItems > 0 && area.maxItems < area.controls.length + 1) { + $scope.$apply(function () { + $(event.target).scope().area.dropNotAllowed = true; + }); + ui.placeholder.hide(); + cancelMove = true; + } else { + if ($(event.target).scope().area.controls.length == 0) { + $scope.$apply(function () { + $(event.target).scope().area.dropOnEmpty = true; + }); + ui.placeholder.hide(); + } else { + ui.placeholder.show(); + } + cancelMove = false; + } + }, + out: function (event, ui) { + $scope.$apply(function () { + $(event.target).scope().area.dropNotAllowed = false; + $(event.target).scope().area.dropOnEmpty = false; + }); + }, + update: function (event, ui) { + /* add all RTEs which are affected by the dragging */ + if (!ui.sender) { + if (cancelMove) { + ui.item.sortable.cancel(); + } + ui.item.parents('.umb-cell.content').find('.mceNoEditor').each(function () { + if ($.inArray($(this).attr('id'), notIncludedRte) < 0) { + notIncludedRte.splice(0, 0, $(this).attr('id')); + } + }); + } else { + $(event.target).find('.mceNoEditor').each(function () { + if ($.inArray($(this).attr('id'), notIncludedRte) < 0) { + notIncludedRte.splice(0, 0, $(this).attr('id')); + } + }); + } + currentForm.$setDirty(); + }, + start: function (e, ui) { + //Get the starting area for reference + var area = $(e.target).scope().area; + startingArea = area; + // fade out control when sorting + ui.item.context.style.display = 'block'; + ui.item.context.style.opacity = '0.5'; + // reset dragged RTE settings in case a RTE isn't dragged + draggedRteSettings = undefined; + ui.item.context.style.display = 'block'; + ui.item.find('.mceNoEditor').each(function () { + notIncludedRte = []; + var editors = _.findWhere(tinyMCE.editors, { id: $(this).attr('id') }); + // save the dragged RTE settings + if (editors) { + draggedRteSettings = editors.settings; + // remove the dragged RTE + tinyMCE.execCommand('mceRemoveEditor', false, $(this).attr('id')); + } + }); + }, + stop: function (e, ui) { + // Fade in control when sorting stops + ui.item.context.style.opacity = '1'; + ui.item.parents('.umb-cell-content').find('.mceNoEditor').each(function () { + if ($.inArray($(this).attr('id'), notIncludedRte) < 0) { + // add all dragged's neighbouring RTEs in the new cell + notIncludedRte.splice(0, 0, $(this).attr('id')); + } + }); + $timeout(function () { + // reconstruct the dragged RTE (could be undefined when dragging something else than RTE) + if (draggedRteSettings !== undefined) { + tinyMCE.init(draggedRteSettings); + } + _.forEach(notIncludedRte, function (id) { + // reset all the other RTEs + if (draggedRteSettings === undefined || id !== draggedRteSettings.id) { + var rteSettings = _.findWhere(tinyMCE.editors, { id: id }).settings; + tinyMCE.execCommand('mceRemoveEditor', false, id); + tinyMCE.init(rteSettings); + } + }); + }, 500, false); + $scope.$apply(function () { + var cell = $(e.target).scope().area; + cell.hasActiveChild = hasActiveChild(cell, cell.controls); + cell.active = false; + }); + } + }; + $scope.toggleSortMode = function () { + $scope.sortMode = !$scope.sortMode; + if ($scope.sortMode) { + $scope.reorderKey = 'general_reorderDone'; + } else { + $scope.reorderKey = 'general_reorder'; + } + }; + $scope.showReorderButton = function () { + if ($scope.model.value && $scope.model.value.sections) { + for (var i = 0; $scope.model.value.sections.length > i; i++) { + var section = $scope.model.value.sections[i]; + if (section.rows && section.rows.length > 0) { + return true; + } + } + } + }; + // ********************************************* + // Add items overlay menu + // ********************************************* + $scope.openEditorOverlay = function (event, area, index, key) { + $scope.editorOverlay = { + view: 'itempicker', + filter: false, + title: localizationService.localize('grid_insertControl'), + availableItems: area.$allowedEditors, + event: event, + show: true, + submit: function (model) { + $scope.addControl(model.selectedItem, area, index); + $scope.editorOverlay.show = false; + $scope.editorOverlay = null; + } + }; + }; + // ********************************************* + // Template management functions + // ********************************************* + $scope.addTemplate = function (template) { + $scope.model.value = angular.copy(template); + //default row data + _.forEach($scope.model.value.sections, function (section) { + $scope.initSection(section); + }); + }; + // ********************************************* + // Row management function + // ********************************************* + $scope.clickRow = function (index, rows) { + rows[index].active = true; + }; + $scope.clickOutsideRow = function (index, rows) { + rows[index].active = false; + }; + function getAllowedLayouts(section) { + var layouts = $scope.model.config.items.layouts; + //This will occur if it is a new section which has been + // created from a 'template' + if (section.allowed && section.allowed.length > 0) { + return _.filter(layouts, function (layout) { + return _.indexOf(section.allowed, layout.name) >= 0; + }); + } else { + return layouts; + } + } + $scope.addRow = function (section, layout) { + //copy the selected layout into the rows collection + var row = angular.copy(layout); + // Init row value + row = $scope.initRow(row); + // Push the new row + if (row) { + section.rows.push(row); + } + currentForm.$setDirty(); + $scope.showRowConfigurations = false; + }; + $scope.removeRow = function (section, $index) { + if (section.rows.length > 0) { + section.rows.splice($index, 1); + $scope.currentRow = null; + $scope.openRTEToolbarId = null; + currentForm.$setDirty(); + } + if (section.rows.length === 0) { + $scope.showRowConfigurations = true; + } + }; + var shouldApply = function (item, itemType, gridItem) { + if (item.applyTo === undefined || item.applyTo === null || item.applyTo === '') { + return true; + } + if (typeof item.applyTo === 'string') { + return item.applyTo === itemType; + } + if (itemType === 'row') { + if (item.applyTo.row === undefined) { + return false; + } + if (item.applyTo.row === null || item.applyTo.row === '') { + return true; + } + var rows = item.applyTo.row.split(','); + return _.indexOf(rows, gridItem.name) !== -1; + } else if (itemType === 'cell') { + if (item.applyTo.cell === undefined) { + return false; + } + if (item.applyTo.cell === null || item.applyTo.cell === '') { + return true; + } + var cells = item.applyTo.cell.split(','); + var cellSize = gridItem.grid.toString(); + return _.indexOf(cells, cellSize) !== -1; + } + }; + $scope.editGridItemSettings = function (gridItem, itemType) { + placeHolder = '{0}'; + var styles, config; + if (itemType === 'control') { + styles = null; + config = angular.copy(gridItem.editor.config.settings); + } else { + styles = _.filter(angular.copy($scope.model.config.items.styles), function (item) { + return shouldApply(item, itemType, gridItem); + }); + config = _.filter(angular.copy($scope.model.config.items.config), function (item) { + return shouldApply(item, itemType, gridItem); + }); + } + if (angular.isObject(gridItem.config)) { + _.each(config, function (cfg) { + var val = gridItem.config[cfg.key]; + if (val) { + cfg.value = stripModifier(val, cfg.modifier); + } + }); + } + if (angular.isObject(gridItem.styles)) { + _.each(styles, function (style) { + var val = gridItem.styles[style.key]; + if (val) { + style.value = stripModifier(val, style.modifier); + } + }); + } + $scope.gridItemSettingsDialog = {}; + $scope.gridItemSettingsDialog.view = 'views/propertyeditors/grid/dialogs/config.html'; + $scope.gridItemSettingsDialog.title = 'Settings'; + $scope.gridItemSettingsDialog.styles = styles; + $scope.gridItemSettingsDialog.config = config; + $scope.gridItemSettingsDialog.show = true; + $scope.gridItemSettingsDialog.submit = function (model) { + var styleObject = {}; + var configObject = {}; + _.each(model.styles, function (style) { + if (style.value) { + styleObject[style.key] = addModifier(style.value, style.modifier); + } + }); + _.each(model.config, function (cfg) { + if (cfg.value) { + configObject[cfg.key] = addModifier(cfg.value, cfg.modifier); + } + }); + gridItem.styles = styleObject; + gridItem.config = configObject; + gridItem.hasConfig = gridItemHasConfig(styleObject, configObject); + currentForm.$setDirty(); + $scope.gridItemSettingsDialog.show = false; + $scope.gridItemSettingsDialog = null; + }; + $scope.gridItemSettingsDialog.close = function (oldModel) { + $scope.gridItemSettingsDialog.show = false; + $scope.gridItemSettingsDialog = null; + }; + }; + function stripModifier(val, modifier) { + if (!val || !modifier || modifier.indexOf(placeHolder) < 0) { + return val; + } else { + var paddArray = modifier.split(placeHolder); + if (paddArray.length == 1) { + if (modifier.indexOf(placeHolder) === 0) { + return val.slice(0, -paddArray[0].length); + } else { + return val.slice(paddArray[0].length, 0); + } + } else { + if (paddArray[1].length === 0) { + return val.slice(paddArray[0].length); + } + return val.slice(paddArray[0].length, -paddArray[1].length); + } + } + } + var addModifier = function (val, modifier) { + if (!modifier || modifier.indexOf(placeHolder) < 0) { + return val; + } else { + return modifier.replace(placeHolder, val); + } + }; + function gridItemHasConfig(styles, config) { + if (_.isEmpty(styles) && _.isEmpty(config)) { + return false; + } else { + return true; + } + } + // ********************************************* + // Area management functions + // ********************************************* + $scope.clickCell = function (index, cells, row) { + cells[index].active = true; + row.hasActiveChild = true; + }; + $scope.clickOutsideCell = function (index, cells, row) { + cells[index].active = false; + row.hasActiveChild = hasActiveChild(row, cells); + }; + $scope.cellPreview = function (cell) { + if (cell && cell.$allowedEditors) { + var editor = cell.$allowedEditors[0]; + return editor.icon; + } else { + return 'icon-layout'; + } + }; + // ********************************************* + // Control management functions + // ********************************************* + $scope.clickControl = function (index, controls, cell) { + controls[index].active = true; + cell.hasActiveChild = true; + }; + $scope.clickOutsideControl = function (index, controls, cell) { + controls[index].active = false; + cell.hasActiveChild = hasActiveChild(cell, controls); + }; + function hasActiveChild(item, children) { + var activeChild = false; + for (var i = 0; children.length > i; i++) { + var child = children[i]; + if (child.active) { + activeChild = true; + } + } + if (activeChild) { + return true; + } + } + var guid = function () { + function s4() { + return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); + } + return function () { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); + }; + }(); + $scope.setUniqueId = function (cell, index) { + return guid(); + }; + $scope.addControl = function (editor, cell, index, initialize) { + initialize = initialize !== false; + var newControl = { + value: null, + editor: editor, + $initializing: initialize + }; + if (index === undefined) { + index = cell.controls.length; + } + newControl.active = true; + //populate control + $scope.initControl(newControl, index + 1); + cell.controls.push(newControl); + }; + $scope.addTinyMce = function (cell) { + var rte = $scope.getEditor('rte'); + $scope.addControl(rte, cell); + }; + $scope.getEditor = function (alias) { + return _.find($scope.availableEditors, function (editor) { + return editor.alias === alias; + }); + }; + $scope.removeControl = function (cell, $index) { + $scope.currentControl = null; + cell.controls.splice($index, 1); + }; + $scope.percentage = function (spans) { + return (spans / $scope.model.config.items.columns * 100).toFixed(8); + }; + $scope.clearPrompt = function (scopedObject, e) { + scopedObject.deletePrompt = false; + e.preventDefault(); + e.stopPropagation(); + }; + $scope.togglePrompt = function (scopedObject) { + scopedObject.deletePrompt = !scopedObject.deletePrompt; + }; + $scope.hidePrompt = function (scopedObject) { + scopedObject.deletePrompt = false; + }; + $scope.toggleAddRow = function () { + $scope.showRowConfigurations = !$scope.showRowConfigurations; + }; + // ********************************************* + // Initialization + // these methods are called from ng-init on the template + // so we can controll their first load data + // + // intialization sets non-saved data like percentage sizing, allowed editors and + // other data that should all be pre-fixed with $ to strip it out on save + // ********************************************* + // ********************************************* + // Init template + sections + // ********************************************* + $scope.initContent = function () { + var clear = true; + //settings indicator shortcut + if ($scope.model.config.items.config && $scope.model.config.items.config.length > 0 || $scope.model.config.items.styles && $scope.model.config.items.styles.length > 0) { + $scope.hasSettings = true; + } + //ensure the grid has a column value set, + //if nothing is found, set it to 12 + if ($scope.model.config.items.columns && angular.isString($scope.model.config.items.columns)) { + $scope.model.config.items.columns = parseInt($scope.model.config.items.columns); + } else { + $scope.model.config.items.columns = 12; + } + if ($scope.model.value && $scope.model.value.sections && $scope.model.value.sections.length > 0 && $scope.model.value.sections[0].rows && $scope.model.value.sections[0].rows.length > 0) { + if ($scope.model.value.name && angular.isArray($scope.model.config.items.templates)) { + //This will occur if it is an existing value, in which case + // we need to determine which layout was applied by looking up + // the name + // TODO: We need to change this to an immutable ID!! + var found = _.find($scope.model.config.items.templates, function (t) { + return t.name === $scope.model.value.name; + }); + if (found && angular.isArray(found.sections) && found.sections.length === $scope.model.value.sections.length) { + //Cool, we've found the template associated with our current value with matching sections counts, now we need to + // merge this template data on to our current value (as if it was new) so that we can preserve what is and isn't + // allowed for this template based on the current config. + _.each(found.sections, function (templateSection, index) { + angular.extend($scope.model.value.sections[index], angular.copy(templateSection)); + }); + } + } + _.forEach($scope.model.value.sections, function (section, index) { + if (section.grid > 0) { + $scope.initSection(section); + //we do this to ensure that the grid can be reset by deleting the last row + if (section.rows.length > 0) { + clear = false; + } + } else { + $scope.model.value.sections.splice(index, 1); + } + }); + } else if ($scope.model.config.items.templates && $scope.model.config.items.templates.length === 1) { + $scope.addTemplate($scope.model.config.items.templates[0]); clear = false; } - - if (clear) { - $scope.model.value = undefined; + if (clear) { + $scope.model.value = undefined; + } + }; + $scope.initSection = function (section) { + section.$percentage = $scope.percentage(section.grid); + section.$allowedLayouts = getAllowedLayouts(section); + if (!section.rows || section.rows.length === 0) { + section.rows = []; + if (section.$allowedLayouts.length === 1) { + $scope.addRow(section, section.$allowedLayouts[0]); + } + } else { + _.forEach(section.rows, function (row, index) { + if (!row.$initialized) { + var initd = $scope.initRow(row); + //if init fails, remove + if (!initd) { + section.rows.splice(index, 1); + } else { + section.rows[index] = initd; + } + } + }); + // if there is more than one row added - hide row add tools + $scope.showRowConfigurations = false; + } + }; + // ********************************************* + // Init layout / row + // ********************************************* + $scope.initRow = function (row) { + //merge the layout data with the original config data + //if there are no config info on this, splice it out + var original = _.find($scope.model.config.items.layouts, function (o) { + return o.name === row.name; + }); + if (!original) { + return null; + } else { + //make a copy to not touch the original config + original = angular.copy(original); + original.styles = row.styles; + original.config = row.config; + original.hasConfig = gridItemHasConfig(row.styles, row.config); + //sync area configuration + _.each(original.areas, function (area, areaIndex) { + if (area.grid > 0) { + var currentArea = row.areas[areaIndex]; + if (currentArea) { + area.config = currentArea.config; + area.styles = currentArea.styles; + area.hasConfig = gridItemHasConfig(currentArea.styles, currentArea.config); + } + //set editor permissions + if (!area.allowed || area.allowAll === true) { + area.$allowedEditors = $scope.availableEditors; + area.$allowsRTE = true; + } else { + area.$allowedEditors = _.filter($scope.availableEditors, function (editor) { + return _.indexOf(area.allowed, editor.alias) >= 0; + }); + if (_.indexOf(area.allowed, 'rte') >= 0) { + area.$allowsRTE = true; + } + } + //copy over existing controls into the new areas + if (row.areas.length > areaIndex && row.areas[areaIndex].controls) { + area.controls = currentArea.controls; + _.forEach(area.controls, function (control, controlIndex) { + $scope.initControl(control, controlIndex); + }); + } else { + //if empty + area.controls = []; + //if only one allowed editor + if (area.$allowedEditors.length === 1) { + $scope.addControl(area.$allowedEditors[0], area, 0, false); + } + } + //set width + area.$percentage = $scope.percentage(area.grid); + area.$uniqueId = $scope.setUniqueId(); + } else { + original.areas.splice(areaIndex, 1); + } + }); + //replace the old row + original.$initialized = true; + //set a disposable unique ID + original.$uniqueId = $scope.setUniqueId(); + //set a no disposable unique ID (util for row styling) + original.id = !row.id ? $scope.setUniqueId() : row.id; + return original; + } + }; + // ********************************************* + // Init control + // ********************************************* + $scope.initControl = function (control, index) { + control.$index = index; + control.$uniqueId = $scope.setUniqueId(); + //error handling in case of missing editor.. + //should only happen if stripped earlier + if (!control.editor) { + control.$editorPath = 'views/propertyeditors/grid/editors/error.html'; + } + if (!control.$editorPath) { + var editorConfig = $scope.getEditor(control.editor.alias); + if (editorConfig) { + control.editor = editorConfig; + //if its an absolute path + if (control.editor.view.startsWith('/') || control.editor.view.startsWith('~/')) { + control.$editorPath = umbRequestHelper.convertVirtualToAbsolutePath(control.editor.view); + } else { + //use convention + control.$editorPath = 'views/propertyeditors/grid/editors/' + control.editor.view + '.html'; + } + } else { + control.$editorPath = 'views/propertyeditors/grid/editors/error.html'; + } + } + }; + gridService.getGridEditors().then(function (response) { + $scope.availableEditors = response.data; + $scope.contentReady = true; + // ********************************************* + // Init grid + // ********************************************* + $scope.initContent(); + }); + //Clean the grid value before submitting to the server, we don't need + // all of that grid configuration in the value to be stored!! All of that + // needs to be merged in at runtime to ensure that the real config values are used + // if they are ever updated. + var unsubscribe = $scope.$on('formSubmitting', function () { + if ($scope.model.value && $scope.model.value.sections) { + _.each($scope.model.value.sections, function (section) { + if (section.rows) { + _.each(section.rows, function (row) { + if (row.areas) { + _.each(row.areas, function (area) { + //Remove the 'editors' - these are the allowed editors, these will + // be injected at runtime to this editor, it should not be persisted + if (area.editors) { + delete area.editors; + } + if (area.controls) { + _.each(area.controls, function (control) { + if (control.editor) { + //replace + var alias = control.editor.alias; + control.editor = { alias: alias }; + } + }); + } + }); + } + }); + } + }); + } + }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + }); + angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditorController', function ($scope, $http, assetsService, $rootScope, dialogService, mediaResource, gridService, imageHelper, $timeout) { + var emptyModel = { + styles: [{ + label: 'Set a background image', + description: 'Set a row background', + key: 'background-image', + view: 'imagepicker', + modifier: 'url({0})' + }], + config: [{ + label: 'Class', + description: 'Set a css class', + key: 'class', + view: 'textstring' + }], + columns: 12, + templates: [ + { + name: '1 column layout', + sections: [{ grid: 12 }] + }, + { + name: '2 column layout', + sections: [ + { grid: 4 }, + { grid: 8 } + ] + } + ], + layouts: [ + { + label: 'Headline', + name: 'Headline', + areas: [{ + grid: 12, + editors: ['headline'] + }] + }, + { + label: 'Article', + name: 'Article', + areas: [ + { grid: 4 }, + { grid: 8 } + ] + } + ] + }; + /**************** + template + *****************/ + $scope.configureTemplate = function (template) { + var templatesCopy = angular.copy($scope.model.value.templates); + if (template === undefined) { + template = { + name: '', + sections: [] + }; + $scope.model.value.templates.push(template); + } + $scope.layoutConfigOverlay = {}; + $scope.layoutConfigOverlay.view = 'views/propertyEditors/grid/dialogs/layoutconfig.html'; + $scope.layoutConfigOverlay.currentLayout = template; + $scope.layoutConfigOverlay.rows = $scope.model.value.layouts; + $scope.layoutConfigOverlay.columns = $scope.model.value.columns; + $scope.layoutConfigOverlay.show = true; + $scope.layoutConfigOverlay.submit = function (model) { + $scope.layoutConfigOverlay.show = false; + $scope.layoutConfigOverlay = null; + }; + $scope.layoutConfigOverlay.close = function (oldModel) { + //reset templates + $scope.model.value.templates = templatesCopy; + $scope.layoutConfigOverlay.show = false; + $scope.layoutConfigOverlay = null; + }; + }; + $scope.deleteTemplate = function (index) { + $scope.model.value.templates.splice(index, 1); + }; + /**************** + Row + *****************/ + $scope.configureLayout = function (layout) { + var layoutsCopy = angular.copy($scope.model.value.layouts); + if (layout === undefined) { + layout = { + name: '', + areas: [] + }; + $scope.model.value.layouts.push(layout); + } + $scope.rowConfigOverlay = {}; + $scope.rowConfigOverlay.view = 'views/propertyEditors/grid/dialogs/rowconfig.html'; + $scope.rowConfigOverlay.currentRow = layout; + $scope.rowConfigOverlay.editors = $scope.editors; + $scope.rowConfigOverlay.columns = $scope.model.value.columns; + $scope.rowConfigOverlay.show = true; + $scope.rowConfigOverlay.submit = function (model) { + $scope.rowConfigOverlay.show = false; + $scope.rowConfigOverlay = null; + }; + $scope.rowConfigOverlay.close = function (oldModel) { + $scope.model.value.layouts = layoutsCopy; + $scope.rowConfigOverlay.show = false; + $scope.rowConfigOverlay = null; + }; + }; + //var rowDeletesPending = false; + $scope.deleteLayout = function (index) { + $scope.rowDeleteOverlay = {}; + $scope.rowDeleteOverlay.view = 'views/propertyEditors/grid/dialogs/rowdeleteconfirm.html'; + $scope.rowDeleteOverlay.dialogData = { rowName: $scope.model.value.layouts[index].name }; + $scope.rowDeleteOverlay.show = true; + $scope.rowDeleteOverlay.submit = function (model) { + $scope.model.value.layouts.splice(index, 1); + $scope.rowDeleteOverlay.show = false; + $scope.rowDeleteOverlay = null; + }; + $scope.rowDeleteOverlay.close = function (oldModel) { + $scope.rowDeleteOverlay.show = false; + $scope.rowDeleteOverlay = null; + }; + }; + /**************** + utillities + *****************/ + $scope.toggleCollection = function (collection, toggle) { + if (toggle) { + collection = []; + } else { + delete collection; + } + }; + $scope.percentage = function (spans) { + return (spans / $scope.model.value.columns * 100).toFixed(8); + }; + $scope.zeroWidthFilter = function (cell) { + return cell.grid > 0; + }; + /**************** + Config + *****************/ + $scope.removeConfigValue = function (collection, index) { + collection.splice(index, 1); + }; + var editConfigCollection = function (configValues, title, callback) { + $scope.editConfigCollectionOverlay = {}; + $scope.editConfigCollectionOverlay.view = 'views/propertyeditors/grid/dialogs/editconfig.html'; + $scope.editConfigCollectionOverlay.config = configValues; + $scope.editConfigCollectionOverlay.title = title; + $scope.editConfigCollectionOverlay.show = true; + $scope.editConfigCollectionOverlay.submit = function (model) { + callback(model.config); + $scope.editConfigCollectionOverlay.show = false; + $scope.editConfigCollectionOverlay = null; + }; + $scope.editConfigCollectionOverlay.close = function (oldModel) { + $scope.editConfigCollectionOverlay.show = false; + $scope.editConfigCollectionOverlay = null; + }; + }; + $scope.editConfig = function () { + editConfigCollection($scope.model.value.config, 'Settings', function (data) { + $scope.model.value.config = data; + }); + }; + $scope.editStyles = function () { + editConfigCollection($scope.model.value.styles, 'Styling', function (data) { + $scope.model.value.styles = data; + }); + }; + /**************** + editors + *****************/ + gridService.getGridEditors().then(function (response) { + $scope.editors = response.data; + }); + /* init grid data */ + if (!$scope.model.value || $scope.model.value === '' || !$scope.model.value.templates) { + $scope.model.value = emptyModel; + } else { + if (!$scope.model.value.columns) { + $scope.model.value.columns = emptyModel.columns; + } + if (!$scope.model.value.config) { + $scope.model.value.config = []; + } + if (!$scope.model.value.styles) { + $scope.model.value.styles = []; + } + } + /**************** + Clean up + *****************/ + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + var ts = $scope.model.value.templates; + var ls = $scope.model.value.layouts; + _.each(ts, function (t) { + _.each(t.sections, function (section, index) { + if (section.grid === 0) { + t.sections.splice(index, 1); + } + }); + }); + _.each(ls, function (l) { + _.each(l.areas, function (area, index) { + if (area.grid === 0) { + l.areas.splice(index, 1); + } + }); + }); + }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + }); + //this controller simply tells the dialogs service to open a mediaPicker window + //with a specified callback, this callback will receive an object with a selection on it + angular.module('umbraco').controller('Umbraco.PropertyEditors.ImageCropperController', function ($rootScope, $routeParams, $scope, $log, mediaHelper, cropperHelper, $timeout, editorState, umbRequestHelper, fileManager, angularHelper) { + var config = angular.copy($scope.model.config); + $scope.imageIsLoaded = false; + //move previously saved value to the editor + if ($scope.model.value) { + //backwards compat with the old file upload (incase some-one swaps them..) + if (angular.isString($scope.model.value)) { + config.src = $scope.model.value; + $scope.model.value = config; + } else if ($scope.model.value.crops) { + //sync any config changes with the editor and drop outdated crops + _.each($scope.model.value.crops, function (saved) { + var configured = _.find(config.crops, function (item) { + return item.alias === saved.alias; + }); + if (configured && configured.height === saved.height && configured.width === saved.width) { + configured.coordinates = saved.coordinates; + } + }); + $scope.model.value.crops = config.crops; + //restore focalpoint if missing + if (!$scope.model.value.focalPoint) { + $scope.model.value.focalPoint = { + left: 0.5, + top: 0.5 + }; + } + } + $scope.imageSrc = $scope.model.value.src; + } + //crop a specific crop + $scope.crop = function (crop) { + $scope.currentCrop = crop; + $scope.currentPoint = undefined; + }; + //done cropping + $scope.done = function () { + $scope.currentCrop = undefined; + $scope.currentPoint = undefined; + }; + //crop a specific crop + $scope.clear = function (crop) { + //clear current uploaded files + fileManager.setFiles($scope.model.alias, []); + //clear the ui + $scope.imageSrc = undefined; + if ($scope.model.value) { + delete $scope.model.value; + } + // set form to dirty to tricker discard changes dialog + var currForm = angularHelper.getCurrentForm($scope); + currForm.$setDirty(); + }; + //show previews + $scope.togglePreviews = function () { + if ($scope.showPreviews) { + $scope.showPreviews = false; + $scope.tempShowPreviews = false; + } else { + $scope.showPreviews = true; + } + }; + $scope.imageLoaded = function () { + $scope.imageIsLoaded = true; + }; + //on image selected, update the cropper + $scope.$on('filesSelected', function (ev, args) { + $scope.model.value = config; + if (args.files && args.files[0]) { + fileManager.setFiles($scope.model.alias, args.files); + var reader = new FileReader(); + reader.onload = function (e) { + $scope.$apply(function () { + $scope.imageSrc = e.target.result; + }); + }; + reader.readAsDataURL(args.files[0]); + } + }); + //here we declare a special method which will be called whenever the value has changed from the server + $scope.model.onValueChanged = function (newVal, oldVal) { + //clear current uploaded files + fileManager.setFiles($scope.model.alias, []); + }; + var unsubscribe = $scope.$on('formSubmitting', function () { + $scope.done(); + }); + $scope.$on('$destroy', function () { + unsubscribe(); + }); + }).run(function (mediaHelper, umbRequestHelper) { + if (mediaHelper && mediaHelper.registerFileResolver) { + //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource + // they contain different data structures so if we need to query against it we need to be aware of this. + mediaHelper.registerFileResolver('Umbraco.ImageCropper', function (property, entity, thumbnail) { + if (property.value && property.value.src) { + if (thumbnail === true) { + return property.value.src + '?width=500&mode=max&animationprocessmode=first'; + } else { + return property.value.src; + } //this is a fallback in case the cropper has been asssigned a upload field + } else if (angular.isString(property.value)) { + if (thumbnail) { + if (mediaHelper.detectIfImageByExtension(property.value)) { + var thumbnailUrl = umbRequestHelper.getApiUrl('imagesApiBaseUrl', 'GetBigThumbnail', [{ originalImagePath: property.value }]); + return thumbnailUrl; + } else { + return null; + } + } else { + return property.value; + } + } + return null; + }); + } + }); + angular.module('umbraco').controller('Umbraco.PrevalueEditors.CropSizesController', function ($scope, $timeout) { + if (!$scope.model.value) { + $scope.model.value = []; + } + $scope.remove = function (item, evt) { + evt.preventDefault(); + $scope.model.value = _.reject($scope.model.value, function (x) { + return x.alias === item.alias; + }); + }; + $scope.edit = function (item, evt) { + evt.preventDefault(); + $scope.newItem = item; + }; + $scope.cancel = function (evt) { + evt.preventDefault(); + $scope.newItem = null; + }; + $scope.add = function (evt) { + evt.preventDefault(); + if ($scope.newItem && $scope.newItem.alias && angular.isNumber($scope.newItem.width) && angular.isNumber($scope.newItem.height) && $scope.newItem.width > 0 && $scope.newItem.height > 0) { + var exists = _.find($scope.model.value, function (item) { + return $scope.newItem.alias === item.alias; + }); + if (!exists) { + $scope.model.value.push($scope.newItem); + $scope.newItem = {}; + $scope.hasError = false; + return; + } + } + //there was an error, do the highlight (will be set back by the directive) + $scope.hasError = true; + }; + }); + function includePropsPreValsController($rootScope, $scope, localizationService, contentTypeResource) { + if (!$scope.model.value) { + $scope.model.value = []; + } + $scope.propertyAliases = []; + $scope.selectedField = null; + $scope.systemFields = [ + { value: 'sortOrder' }, + { value: 'updateDate' }, + { value: 'updater' }, + { value: 'createDate' }, + { value: 'owner' }, + { value: 'published' }, + { value: 'contentTypeAlias' }, + { value: 'email' }, + { value: 'username' } + ]; + $scope.getLocalizedKey = function (alias) { + switch (alias) { + case 'name': + return 'general_name'; + case 'sortOrder': + return 'general_sort'; + case 'updateDate': + return 'content_updateDate'; + case 'updater': + return 'content_updatedBy'; + case 'createDate': + return 'content_createDate'; + case 'owner': + return 'content_createBy'; + case 'published': + return 'content_isPublished'; + case 'contentTypeAlias': + //NOTE: This will just be 'Document' type even if it's for media/members since this is just a pre-val editor and we don't have a key for 'Content Type Alias' + return 'content_documentType'; + case 'email': + return 'general_email'; + case 'username': + return 'general_username'; + } + return alias; + }; + $scope.removeField = function (e) { + $scope.model.value = _.reject($scope.model.value, function (x) { + return x.alias === e.alias; + }); + }; + //now we'll localize these strings, for some reason the directive doesn't work inside of the select group with an ng-model declared + _.each($scope.systemFields, function (e, i) { + var key = $scope.getLocalizedKey(e.value); + localizationService.localize(key).then(function (v) { + e.name = v; + switch (e.value) { + case 'updater': + e.name += ' (Content only)'; + break; + case 'published': + e.name += ' (Content only)'; + break; + case 'email': + e.name += ' (Members only)'; + break; + case 'username': + e.name += ' (Members only)'; + break; + } + }); + }); + // Return a helper with preserved width of cells + var fixHelper = function (e, ui) { + var h = ui.clone(); + h.children().each(function () { + $(this).width($(this).width()); + }); + h.css('background-color', 'lightgray'); + return h; + }; + $scope.sortableOptions = { + helper: fixHelper, + handle: '.handle', + opacity: 0.5, + axis: 'y', + containment: 'parent', + cursor: 'move', + items: '> tr', + tolerance: 'pointer', + update: function (e, ui) { + // Get the new and old index for the moved element (using the text as the identifier) + var newIndex = ui.item.index(); + var movedAlias = $('.alias-value', ui.item).text().trim(); + var originalIndex = getAliasIndexByText(movedAlias); + // Move the element in the model + if (originalIndex > -1) { + var movedElement = $scope.model.value[originalIndex]; + $scope.model.value.splice(originalIndex, 1); + $scope.model.value.splice(newIndex, 0, movedElement); + } + } + }; + contentTypeResource.getAllPropertyTypeAliases().then(function (data) { + $scope.propertyAliases = data; + }); + $scope.addField = function () { + var val = $scope.selectedField; + var isSystem = val.startsWith('_system_'); + if (isSystem) { + val = val.trimStart('_system_'); + } + var exists = _.find($scope.model.value, function (i) { + return i.alias === val; + }); + if (!exists) { + $scope.model.value.push({ + alias: val, + isSystem: isSystem ? 1 : 0 + }); + } + }; + function getAliasIndexByText(value) { + for (var i = 0; i < $scope.model.value.length; i++) { + if ($scope.model.value[i].alias === value) { + return i; + } + } + return -1; + } + } + angular.module('umbraco').controller('Umbraco.PrevalueEditors.IncludePropertiesListViewController', includePropsPreValsController); + /** + * @ngdoc controller + * @name Umbraco.PrevalueEditors.ListViewLayoutsPreValsController + * @function + * + * @description + * The controller for configuring layouts for list views + */ + (function () { + 'use strict'; + function ListViewLayoutsPreValsController($scope) { + var vm = this; + vm.focusLayoutName = false; + vm.layoutsSortableOptions = { + distance: 10, + tolerance: 'pointer', + opacity: 0.7, + scroll: true, + cursor: 'move', + handle: '.list-view-layout__sort-handle' + }; + vm.addLayout = addLayout; + vm.showPrompt = showPrompt; + vm.hidePrompt = hidePrompt; + vm.removeLayout = removeLayout; + vm.openIconPicker = openIconPicker; + function activate() { + } + function addLayout() { + vm.focusLayoutName = false; + var layout = { + 'name': '', + 'path': '', + 'icon': 'icon-stop', + 'selected': true + }; + $scope.model.value.push(layout); + } + function showPrompt(layout) { + layout.deletePrompt = true; + } + function hidePrompt(layout) { + layout.deletePrompt = false; + } + function removeLayout($index, layout) { + $scope.model.value.splice($index, 1); + } + function openIconPicker(layout) { + vm.iconPickerDialog = { + view: 'iconpicker', + show: true, + submit: function (model) { + if (model.color) { + layout.icon = model.icon + ' ' + model.color; + } else { + layout.icon = model.icon; + } + vm.focusLayoutName = true; + vm.iconPickerDialog.show = false; + vm.iconPickerDialog = null; + } + }; + } + activate(); + } + angular.module('umbraco').controller('Umbraco.PrevalueEditors.ListViewLayoutsPreValsController', ListViewLayoutsPreValsController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.EditController + * @function + * + * @description + * The controller for the content type editor + */ + (function () { + 'use strict'; + function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource, $location, listViewHelper, mediaTypeHelper) { + var vm = this; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; + vm.nodeId = $scope.contentId; + // Use whitelist of allowed file types if provided + vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); + if (vm.acceptedFileTypes === '') { + // If not provided, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); + } + vm.maxFileSize = umbracoSettings.maxFileSize + 'KB'; + vm.activeDrag = false; + vm.mediaDetailsTooltip = {}; + vm.itemsWithoutFolders = []; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + vm.acceptedMediatypes = []; + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; + vm.hoverMediaItemDetails = hoverMediaItemDetails; + vm.selectContentItem = selectContentItem; + vm.selectItem = selectItem; + vm.selectFolder = selectFolder; + vm.goToItem = goToItem; + function activate() { + vm.itemsWithoutFolders = filterOutFolders($scope.items); + //no need to make another REST/DB call if this data is not used when we are browsing the bin + if ($scope.entityType === 'media' && !vm.isRecycleBin) { + mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { + vm.acceptedMediatypes = types; + }); + } + } + function filterOutFolders(items) { + var newArray = []; + if (items && items.length) { + for (var i = 0; items.length > i; i++) { + var item = items[i]; + var isFolder = !mediaHelper.hasFilePropertyType(item); + if (!isFolder) { + newArray.push(item); + } + } + } + return newArray; + } + function dragEnter(el, event) { + vm.activeDrag = true; + } + function dragLeave(el, event) { + vm.activeDrag = false; + } + function onFilesQueue() { + vm.activeDrag = false; + } + function onUploadComplete() { + $scope.getContent($scope.contentId); + } + function hoverMediaItemDetails(item, event, hover) { + if (hover && !vm.mediaDetailsTooltip.show) { + vm.mediaDetailsTooltip.event = event; + vm.mediaDetailsTooltip.item = item; + vm.mediaDetailsTooltip.show = true; + } else if (!hover && vm.mediaDetailsTooltip.show) { + vm.mediaDetailsTooltip.show = false; + } + } + function selectContentItem(item, $event, $index) { + listViewHelper.selectHandler(item, $index, $scope.items, $scope.selection, $event); + } + function selectItem(item, $event, $index) { + listViewHelper.selectHandler(item, $index, vm.itemsWithoutFolders, $scope.selection, $event); + } + function selectFolder(folder, $event, $index) { + listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event); + } + function goToItem(item, $event, $index) { + $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); + } + activate(); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.ListView.GridLayoutController', ListViewGridLayoutController); + }()); + (function () { + 'use strict'; + function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) { + var vm = this; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; + vm.nodeId = $scope.contentId; + // Use whitelist of allowed file types if provided + vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); + if (vm.acceptedFileTypes === '') { + // If not provided, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); + } + vm.maxFileSize = umbracoSettings.maxFileSize + 'KB'; + vm.activeDrag = false; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + vm.acceptedMediatypes = []; + vm.selectItem = selectItem; + vm.clickItem = clickItem; + vm.selectAll = selectAll; + vm.isSelectedAll = isSelectedAll; + vm.isSortDirection = isSortDirection; + vm.sort = sort; + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; + function activate() { + if ($scope.entityType === 'media') { + mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { + vm.acceptedMediatypes = types; + }); + } + } + function selectAll($event) { + listViewHelper.selectAllItems($scope.items, $scope.selection, $event); + } + function isSelectedAll() { + return listViewHelper.isSelectedAll($scope.items, $scope.selection); + } + function selectItem(selectedItem, $index, $event) { + listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); + } + function clickItem(item) { + // if item.id is 2147483647 (int.MaxValue) use item.key + $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id)); + } + function isSortDirection(col, direction) { + return listViewHelper.setSortingDirection(col, direction, $scope.options); + } + function sort(field, allow, isSystem) { + if (allow) { + $scope.options.orderBySystemField = isSystem; + listViewHelper.setSorting(field, allow, $scope.options); + $scope.getContent($scope.contentId); + } + } + // Dropzone upload functions + function dragEnter(el, event) { + vm.activeDrag = true; + } + function dragLeave(el, event) { + vm.activeDrag = false; + } + function onFilesQueue() { + vm.activeDrag = false; + } + function onUploadComplete() { + $scope.getContent($scope.contentId); + } + activate(); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.ListView.ListLayoutController', ListViewListLayoutController); + }()); + function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService, navigationService, treeService) { + //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content + // that isn't created yet, if we continue this will use the parent id in the route params which isn't what + // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove + // the list view tab entirely when it's new. + if ($routeParams.create) { + $scope.isNew = true; + return; + } + //Now we need to check if this is for media, members or content because that will depend on the resources we use + var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if ($scope.model.config.entityType && $scope.model.config.entityType === 'member' || appState.getSectionState('currentSection') === 'member') { + $scope.entityType = 'member'; + contentResource = $injector.get('memberResource'); + getContentTypesCallback = $injector.get('memberTypeResource').getTypes; + getListResultsCallback = contentResource.getPagedResults; + deleteItemCallback = contentResource.deleteByKey; + getIdCallback = function (selected) { + return selected.key; + }; + createEditUrlCallback = function (item) { + return '/' + $scope.entityType + '/' + $scope.entityType + '/edit/' + item.key + '?page=' + $scope.options.pageNumber + '&listName=' + $scope.contentId; + }; + } else { + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if ($scope.model.config.entityType && $scope.model.config.entityType === 'media' || appState.getSectionState('currentSection') === 'media') { + $scope.entityType = 'media'; + contentResource = $injector.get('mediaResource'); + getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; + } else { + $scope.entityType = 'content'; + contentResource = $injector.get('contentResource'); + getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; + } + getListResultsCallback = contentResource.getChildren; + deleteItemCallback = contentResource.deleteById; + getIdCallback = function (selected) { + return selected.id; + }; + createEditUrlCallback = function (item) { + return '/' + $scope.entityType + '/' + $scope.entityType + '/edit/' + item.id + '?page=' + $scope.options.pageNumber; + }; + } + $scope.pagination = []; + $scope.isNew = false; + $scope.actionInProgress = false; + $scope.selection = []; + $scope.folders = []; + $scope.listViewResultSet = { + totalPages: 0, + items: [] + }; + $scope.currentNodePermissions = {}; + //Just ensure we do have an editorState + if (editorState.current) { + //Fetch current node allowed actions for the current user + //This is the current node & not each individual child node in the list + var currentUserPermissions = editorState.current.allowedActions; + //Create a nicer model rather than the funky & hard to remember permissions strings + $scope.currentNodePermissions = { + 'canCopy': _.contains(currentUserPermissions, 'O'), + //Magic Char = O + 'canCreate': _.contains(currentUserPermissions, 'C'), + //Magic Char = C + 'canDelete': _.contains(currentUserPermissions, 'D'), + //Magic Char = D + 'canMove': _.contains(currentUserPermissions, 'M'), + //Magic Char = M + 'canPublish': _.contains(currentUserPermissions, 'U'), + //Magic Char = U + 'canUnpublish': _.contains(currentUserPermissions, 'U') + }; + } + //when this is null, we don't check permissions + $scope.buttonPermissions = null; + //When we are dealing with 'content', we need to deal with permissions on child nodes. + // Currently there is no real good way to + if ($scope.entityType === 'content') { + var idsWithPermissions = null; + $scope.buttonPermissions = { + canCopy: true, + canCreate: true, + canDelete: true, + canMove: true, + canPublish: true, + canUnpublish: true + }; + $scope.$watch(function () { + return $scope.selection.length; + }, function (newVal, oldVal) { + if (idsWithPermissions == null && newVal > 0 || idsWithPermissions != null) { + //get all of the selected ids + var ids = _.map($scope.selection, function (i) { + return i.id.toString(); + }); + //remove the dictionary items that don't have matching ids + var filtered = {}; + _.each(idsWithPermissions, function (value, key, list) { + if (_.contains(ids, key)) { + filtered[key] = value; + } + }); + idsWithPermissions = filtered; + //find all ids that we haven't looked up permissions for + var existingIds = _.keys(idsWithPermissions); + var missingLookup = _.map(_.difference(ids, existingIds), function (i) { + return Number(i); + }); + if (missingLookup.length > 0) { + contentResource.getPermissions(missingLookup).then(function (p) { + $scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions); + }); + } else { + $scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions); + } + } + }); + } + $scope.options = { + displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, + pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, + pageNumber: $routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0 ? $routeParams.page : 1, + filter: '', + orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), + orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : 'desc', + orderBySystemField: true, + includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ + { + alias: 'updateDate', + header: 'Last edited', + isSystem: 1 + }, + { + alias: 'updater', + header: 'Last edited by', + isSystem: 1 + } + ], + layout: { + layouts: $scope.model.config.layouts, + activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) + }, + allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, + allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, + allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, + allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, + allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete + }; + // Check if selected order by field is actually custom field + for (var j = 0; j < $scope.options.includeProperties.length; j++) { + var includedProperty = $scope.options.includeProperties[j]; + if (includedProperty.alias.toLowerCase() === $scope.options.orderBy.toLowerCase()) { + $scope.options.orderBySystemField = includedProperty.isSystem === 1; + break; + } + } + //update all of the system includeProperties to enable sorting + _.each($scope.options.includeProperties, function (e, i) { + //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted + // to do that, we'd need to update the base query for content to include the content type alias column + // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? + if (e.alias != 'contentTypeAlias') { + e.allowSorting = true; + } + // Another special case for members, only fields on the base table (cmsMember) can be used for sorting + if (e.isSystem && $scope.entityType == 'member') { + e.allowSorting = e.alias == 'username' || e.alias == 'email'; + } + if (e.isSystem) { + //localize the header + var key = getLocalizedKey(e.alias); + localizationService.localize(key).then(function (v) { + e.header = v; + }); + } + }); + $scope.selectLayout = function (selectedLayout) { + $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); + }; + function showNotificationsAndReset(err, reload, successMsg) { + //check if response is ysod + if (err.status && err.status >= 500) { + // Open ysod overlay + $scope.ysodOverlay = { + view: 'ysod', + error: err, + show: true + }; + } + $timeout(function () { + $scope.bulkStatus = ''; + $scope.actionInProgress = false; + }, 500); + if (reload === true) { + $scope.reloadView($scope.contentId, true); + } + if (err.data && angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } else if (successMsg) { + localizationService.localize('bulk_done').then(function (v) { + notificationsService.success(v, successMsg); + }); + } + } + $scope.next = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + $scope.goToPage = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + $scope.prev = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + /*Loads the search results, based on parameters set in prev,next,sort and so on*/ + /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state + with simple values */ + $scope.getContent = function () { + $scope.reloadView($scope.contentId, true); + }; + $scope.reloadView = function (id, reloadFolders) { + $scope.viewLoaded = false; + listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); + getListResultsCallback(id, $scope.options).then(function (data) { + $scope.actionInProgress = false; + $scope.listViewResultSet = data; + //update all values for display + if ($scope.listViewResultSet.items) { + _.each($scope.listViewResultSet.items, function (e, index) { + setPropertyValues(e); + }); + } + if (reloadFolders && $scope.entityType === 'media') { + //The folders aren't loaded - we only need to do this once since we're never changing node ids + mediaResource.getChildFolders($scope.contentId).then(function (folders) { + $scope.folders = folders; + $scope.viewLoaded = true; + }); + } else { + $scope.viewLoaded = true; + } + //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example + // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last + // available page and then re-load again + if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { + $scope.options.pageNumber = $scope.listViewResultSet.totalPages; + //reload! + $scope.reloadView(id); + } + }); + }; + var searchListView = _.debounce(function () { + $scope.$apply(function () { + makeSearch(); + }); + }, 500); + $scope.forceSearch = function (ev) { + //13: enter + switch (ev.keyCode) { + case 13: + makeSearch(); + break; + } + }; + $scope.enterSearch = function () { + $scope.viewLoaded = false; + searchListView(); + }; + function makeSearch() { + if ($scope.options.filter !== null && $scope.options.filter !== undefined) { + $scope.options.pageNumber = 1; + $scope.reloadView($scope.contentId); + } + } + $scope.isAnythingSelected = function () { + if ($scope.selection.length === 0) { + return false; + } else { + return true; + } + }; + $scope.selectedItemsCount = function () { + return $scope.selection.length; + }; + $scope.clearSelection = function () { + listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); + }; + $scope.getIcon = function (entry) { + return iconHelper.convertFromLegacyIcon(entry.icon); + }; + function serial(selected, fn, getStatusMsg, index) { + return fn(selected, index).then(function (content) { + index++; + $scope.bulkStatus = getStatusMsg(index, selected.length); + return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; + }, function (err) { + var reload = index > 0; + showNotificationsAndReset(err, reload); + return err; + }); + } + function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { + var selected = $scope.selection; + if (selected.length === 0) + return; + if (confirmMsg && !confirm(confirmMsg)) + return; + $scope.actionInProgress = true; + $scope.bulkStatus = getStatusMsg(0, selected.length); + return serial(selected, fn, getStatusMsg, 0).then(function (result) { + // executes once the whole selection has been processed + // in case of an error (caught by serial), result will be the error + if (!(result.data && angular.isArray(result.data.notifications))) + showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); + }); + } + $scope.delete = function () { + var confirmDeleteText = ''; + localizationService.localize('defaultdialogs_confirmdelete').then(function (value) { + confirmDeleteText = value; + var attempt = applySelected(function (selected, index) { + return deleteItemCallback(getIdCallback(selected[index])); + }, function (count, total) { + var key = total === 1 ? 'bulk_deletedItemOfItem' : 'bulk_deletedItemOfItems'; + return localizationService.localize(key, [ + count, + total + ]); + }, function (total) { + var key = total === 1 ? 'bulk_deletedItem' : 'bulk_deletedItems'; + return localizationService.localize(key, [total]); + }, confirmDeleteText + '?'); + if (attempt) { + attempt.then(function () { + //executes if all is successful, let's sync the tree + var activeNode = appState.getTreeState('selectedNode'); + if (activeNode) { + navigationService.reloadNode(activeNode); + } + }); + } + }); + }; + $scope.publish = function () { + applySelected(function (selected, index) { + return contentResource.publishById(getIdCallback(selected[index])); + }, function (count, total) { + var key = total === 1 ? 'bulk_publishedItemOfItem' : 'bulk_publishedItemOfItems'; + return localizationService.localize(key, [ + count, + total + ]); + }, function (total) { + var key = total === 1 ? 'bulk_publishedItem' : 'bulk_publishedItems'; + return localizationService.localize(key, [total]); + }); + }; + $scope.unpublish = function () { + applySelected(function (selected, index) { + return contentResource.unPublish(getIdCallback(selected[index])); + }, function (count, total) { + var key = total === 1 ? 'bulk_unpublishedItemOfItem' : 'bulk_unpublishedItemOfItems'; + return localizationService.localize(key, [ + count, + total + ]); + }, function (total) { + var key = total === 1 ? 'bulk_unpublishedItem' : 'bulk_unpublishedItems'; + return localizationService.localize(key, [total]); + }); + }; + $scope.move = function () { + $scope.moveDialog = {}; + $scope.moveDialog.title = localizationService.localize('general_move'); + $scope.moveDialog.section = $scope.entityType; + $scope.moveDialog.currentNode = $scope.contentId; + $scope.moveDialog.view = 'move'; + $scope.moveDialog.show = true; + $scope.moveDialog.submit = function (model) { + if (model.target) { + performMove(model.target); + } + $scope.moveDialog.show = false; + $scope.moveDialog = null; + }; + $scope.moveDialog.close = function (oldModel) { + $scope.moveDialog.show = false; + $scope.moveDialog = null; + }; + }; + function performMove(target) { + //NOTE: With the way this applySelected/serial works, I'm not sure there's a better way currently to return + // a specific value from one of the methods, so we'll have to try this way. Even though the first method + // will fire once per every node moved, the destination path will be the same and we need to use that to sync. + var newPath = null; + applySelected(function (selected, index) { + return contentResource.move({ + parentId: target.id, + id: getIdCallback(selected[index]) + }).then(function (path) { + newPath = path; + return path; + }); + }, function (count, total) { + var key = total === 1 ? 'bulk_movedItemOfItem' : 'bulk_movedItemOfItems'; + return localizationService.localize(key, [ + count, + total + ]); + }, function (total) { + var key = total === 1 ? 'bulk_movedItem' : 'bulk_movedItems'; + return localizationService.localize(key, [total]); + }).then(function () { + //executes if all is successful, let's sync the tree + if (newPath) { + //we need to do a double sync here: first refresh the node where the content was moved, + // then refresh the node where the content was moved from + navigationService.syncTree({ + tree: target.nodeType ? target.nodeType : target.metaData.treeAlias, + path: newPath, + forceReload: true, + activate: false + }).then(function (args) { + //get the currently edited node (if any) + var activeNode = appState.getTreeState('selectedNode'); + if (activeNode) { + navigationService.reloadNode(activeNode); + } + }); + } + }); + } + $scope.copy = function () { + $scope.copyDialog = {}; + $scope.copyDialog.title = localizationService.localize('general_copy'); + $scope.copyDialog.section = $scope.entityType; + $scope.copyDialog.currentNode = $scope.contentId; + $scope.copyDialog.view = 'copy'; + $scope.copyDialog.show = true; + $scope.copyDialog.submit = function (model) { + if (model.target) { + performCopy(model.target, model.relateToOriginal); + } + $scope.copyDialog.show = false; + $scope.copyDialog = null; + }; + $scope.copyDialog.close = function (oldModel) { + $scope.copyDialog.show = false; + $scope.copyDialog = null; + }; + }; + function performCopy(target, relateToOriginal) { + applySelected(function (selected, index) { + return contentResource.copy({ + parentId: target.id, + id: getIdCallback(selected[index]), + relateToOriginal: relateToOriginal + }); + }, function (count, total) { + var key = total === 1 ? 'bulk_copiedItemOfItem' : 'bulk_copiedItemOfItems'; + return localizationService.localize(key, [ + count, + total + ]); + }, function (total) { + var key = total === 1 ? 'bulk_copiedItem' : 'bulk_copiedItems'; + return localizationService.localize(key, [total]); + }); + } + function getCustomPropertyValue(alias, properties) { + var value = ''; + var index = 0; + var foundAlias = false; + for (var i = 0; i < properties.length; i++) { + if (properties[i].alias == alias) { + foundAlias = true; + break; + } + index++; + } + if (foundAlias) { + value = properties[index].value; + } + return value; + } + /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ + function setPropertyValues(result) { + //set the edit url + result.editPath = createEditUrlCallback(result); + _.each($scope.options.includeProperties, function (e, i) { + var alias = e.alias; + // First try to pull the value directly from the alias (e.g. updatedBy) + var value = result[alias]; + // If this returns an object, look for the name property of that (e.g. owner.name) + if (value === Object(value)) { + value = value['name']; + } + // If we've got nothing yet, look at a user defined property + if (typeof value === 'undefined') { + value = getCustomPropertyValue(alias, result.properties); + } + // If we have a date, format it + if (isDate(value)) { + value = value.substring(0, value.length - 3); + } + // set what we've got on the result + result[alias] = value; + }); + } + function isDate(val) { + if (angular.isString(val)) { + return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); + } + return false; + } + function initView() { + //default to root id if the id is undefined + var id = $routeParams.id; + if (id === undefined) { + id = -1; + } + $scope.listViewAllowedTypes = getContentTypesCallback(id); + $scope.contentId = id; + $scope.isTrashed = id === '-20' || id === '-21'; + $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; + $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; + $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || $scope.options.allowBulkUnpublish || $scope.options.allowBulkCopy || $scope.options.allowBulkMove || $scope.options.allowBulkDelete; + $scope.reloadView($scope.contentId, true); + } + function getLocalizedKey(alias) { + switch (alias) { + case 'sortOrder': + return 'general_sort'; + case 'updateDate': + return 'content_updateDate'; + case 'updater': + return 'content_updatedBy'; + case 'createDate': + return 'content_createDate'; + case 'owner': + return 'content_createBy'; + case 'published': + return 'content_isPublished'; + case 'contentTypeAlias': + //TODO: Check for members + return $scope.entityType === 'content' ? 'content_documentType' : 'content_mediatype'; + case 'email': + return 'general_email'; + case 'username': + return 'general_username'; + } + return alias; + } + function getItemKey(itemId) { + for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { + var item = $scope.listViewResultSet.items[i]; + if (item.id === itemId) { + return item.key; + } + } + } + //GO! + initView(); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.ListViewController', listViewController); + function sortByPreValsController($rootScope, $scope, localizationService, editorState, listViewPrevalueHelper) { + //Get the prevalue from the correct place + function getPrevalues() { + if (editorState.current.preValues) { + return editorState.current.preValues; + } else { + return listViewPrevalueHelper.getPrevalues(); + } + } + //Watch the prevalues + $scope.$watch(function () { + return _.findWhere(getPrevalues(), { key: 'includeProperties' }).value; + }, function () { + populateFields(); + }, true); + //Use deep watching, otherwise we won't pick up header changes + function populateFields() { + // Helper to find a particular value from the list of sort by options + function findFromSortByFields(value) { + return _.find($scope.sortByFields, function (e) { + return e.value.toLowerCase() === value.toLowerCase(); + }); + } + // Get list of properties assigned as columns of the list view + var propsPreValue = _.findWhere(getPrevalues(), { key: 'includeProperties' }); + // Populate list of options for the default sort (all the columns plus then node name) + $scope.sortByFields = []; + $scope.sortByFields.push({ + value: 'name', + name: 'Name', + isSystem: 1 + }); + if (propsPreValue != undefined) { + for (var i = 0; i < propsPreValue.value.length; i++) { + var value = propsPreValue.value[i]; + $scope.sortByFields.push({ + value: value.alias, + name: value.header, + isSystem: value.isSystem + }); + } + } + // Localize the system fields, for some reason the directive doesn't work inside of the select group with an ng-model declared + var systemFields = [ + { + value: 'SortOrder', + key: 'general_sort' + }, + { + value: 'Name', + key: 'general_name' + }, + { + value: 'VersionDate', + key: 'content_updateDate' + }, + { + value: 'Updater', + key: 'content_updatedBy' + }, + { + value: 'CreateDate', + key: 'content_createDate' + }, + { + value: 'Owner', + key: 'content_createBy' + }, + { + value: 'ContentTypeAlias', + key: 'content_documentType' + }, + { + value: 'Published', + key: 'content_isPublished' + }, + { + value: 'Email', + key: 'general_email' + }, + { + value: 'Username', + key: 'general_username' + } + ]; + _.each(systemFields, function (e) { + localizationService.localize(e.key).then(function (v) { + var sortByListValue = findFromSortByFields(e.value); + if (sortByListValue) { + sortByListValue.name = v; + switch (e.value) { + case 'Updater': + e.name += ' (Content only)'; + break; + case 'Published': + e.name += ' (Content only)'; + break; + case 'Email': + e.name += ' (Members only)'; + break; + case 'Username': + e.name += ' (Members only)'; + break; + } + } + }); + }); + // Check existing model value is available in list and ensure a value is set + var existingValue = findFromSortByFields($scope.model.value); + if (existingValue) { + // Set the existing value + // The old implementation pre Umbraco 7.5 used PascalCase aliases, this uses camelCase, so this ensures that any previous value is set + $scope.model.value = existingValue.value; + } else { + // Existing value not found, set to first value + $scope.model.value = $scope.sortByFields[0].value; + } + } + } + angular.module('umbraco').controller('Umbraco.PrevalueEditors.SortByListViewController', sortByPreValsController); + //DO NOT DELETE THIS, this is in use... + angular.module('umbraco').controller('Umbraco.PropertyEditors.MacroContainerController', function ($scope, dialogService, entityResource, macroService) { + $scope.renderModel = []; + $scope.allowOpenButton = true; + $scope.allowRemoveButton = true; + $scope.sortableOptions = {}; + if ($scope.model.value) { + var macros = $scope.model.value.split('>'); + angular.forEach(macros, function (syntax, key) { + if (syntax && syntax.length > 10) { + //re-add the char we split on + syntax = syntax + '>'; + var parsed = macroService.parseMacroSyntax(syntax); + if (!parsed) { + parsed = {}; + } + parsed.syntax = syntax; + collectDetails(parsed); + $scope.renderModel.push(parsed); + setSortingState($scope.renderModel); + } + }); + } + function collectDetails(macro) { + macro.details = ''; + macro.icon = 'icon-settings-alt'; + if (macro.macroParamsDictionary) { + angular.forEach(macro.macroParamsDictionary, function (value, key) { + macro.details += key + ': ' + value + ' '; + }); + } + } + function openDialog(index) { + var dialogData = { allowedMacros: $scope.model.config.allowed }; + if (index !== null && $scope.renderModel[index]) { + var macro = $scope.renderModel[index]; + dialogData['macroData'] = macro; + } + $scope.macroPickerOverlay = {}; + $scope.macroPickerOverlay.view = 'macropicker'; + $scope.macroPickerOverlay.dialogData = dialogData; + $scope.macroPickerOverlay.show = true; + $scope.macroPickerOverlay.submit = function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + collectDetails(macroObject); + //update the raw syntax and the list... + if (index !== null && $scope.renderModel[index]) { + $scope.renderModel[index] = macroObject; + } else { + $scope.renderModel.push(macroObject); + } + setSortingState($scope.renderModel); + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + $scope.macroPickerOverlay.close = function (oldModel) { + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + } + $scope.edit = function (index) { + openDialog(index); + }; + $scope.add = function () { + if ($scope.model.config.max && $scope.model.config.max > 0 && $scope.renderModel.length >= $scope.model.config.max) { + //cannot add more than the max + return; + } + openDialog(); + }; + $scope.remove = function (index) { + $scope.renderModel.splice(index, 1); + setSortingState($scope.renderModel); + }; + $scope.clear = function () { + $scope.model.value = ''; + $scope.renderModel = []; + }; + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + var syntax = []; + angular.forEach($scope.renderModel, function (value, key) { + syntax.push(value.syntax); + }); + $scope.model.value = syntax.join(''); + }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + function trim(str, chr) { + var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); + return str.replace(rgxtrim, ''); + } + function setSortingState(items) { + // disable sorting if the list only consist of one item + if (items.length > 1) { + $scope.sortableOptions.disabled = false; + } else { + $scope.sortableOptions.disabled = true; + } + } + }); + function MacroListController($scope, entityResource) { + $scope.items = []; + entityResource.getAll('Macro').then(function (items) { + _.each(items, function (i) { + $scope.items.push({ + name: i.name, + alias: i.alias + }); + }); + }); + } + angular.module('umbraco').controller('Umbraco.PrevalueEditors.MacroList', MacroListController); + //inject umbracos assetsServce and dialog service + function MarkdownEditorController($scope, $element, assetsService, dialogService, angularHelper, $timeout) { + //tell the assets service to load the markdown.editor libs from the markdown editors + //plugin folder + if ($scope.model.value === null || $scope.model.value === '') { + $scope.model.value = $scope.model.config.defaultValue; + } + function openMediaPicker(callback) { + $scope.mediaPickerOverlay = {}; + $scope.mediaPickerOverlay.view = 'mediaPicker'; + $scope.mediaPickerOverlay.show = true; + $scope.mediaPickerOverlay.disableFolderSelect = true; + $scope.mediaPickerOverlay.submit = function (model) { + var selectedImagePath = model.selectedImages[0].image; + callback(selectedImagePath); + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; + $scope.mediaPickerOverlay.close = function (model) { + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; + } + assetsService.load([ + 'lib/markdown/markdown.converter.js', + 'lib/markdown/markdown.sanitizer.js', + 'lib/markdown/markdown.editor.js' + ]).then(function () { + // we need a short delay to wait for the textbox to appear. + setTimeout(function () { + //this function will execute when all dependencies have loaded + // but in the case that they've been previously loaded, we can only + // init the md editor after this digest because the DOM needs to be ready first + // so run the init on a timeout + $timeout(function () { + var converter2 = new Markdown.Converter(); + var editor2 = new Markdown.Editor(converter2, '-' + $scope.model.alias); + editor2.run(); + //subscribe to the image dialog clicks + editor2.hooks.set('insertImageDialog', function (callback) { + openMediaPicker(callback); + return true; // tell the editor that we'll take care of getting the image url + }); + editor2.hooks.set('onPreviewRefresh', function () { + // We must manually update the model as there is no way to hook into the markdown editor events without exstensive edits to the library. + if ($scope.model.value !== $('textarea', $element).val()) { + angularHelper.getCurrentForm($scope).$setDirty(); + $scope.model.value = $('textarea', $element).val(); + } + }); + }, 200); + }); + //load the seperat css for the editor to avoid it blocking our js loading TEMP HACK + assetsService.loadCss('lib/markdown/markdown.css'); + }); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.MarkdownEditorController', MarkdownEditorController); + //this controller simply tells the dialogs service to open a mediaPicker window + //with a specified callback, this callback will receive an object with a selection on it + angular.module('umbraco').controller('Umbraco.PropertyEditors.MediaPickerController', function ($rootScope, $scope, dialogService, entityResource, mediaResource, mediaHelper, $timeout, userService, $location, localizationService) { + //check the pre-values for multi-picker + var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; + var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false; + var disableFolderSelect = $scope.model.config.disableFolderSelect && $scope.model.config.disableFolderSelect !== '0' ? true : false; + if (!$scope.model.config.startNodeId) { + userService.getCurrentUser().then(function (userData) { + $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; + }); + } + function setupViewModel() { + $scope.images = []; + $scope.ids = []; + if ($scope.model.value) { + var ids = $scope.model.value.split(','); + //NOTE: We need to use the entityResource NOT the mediaResource here because + // the mediaResource has server side auth configured for which the user must have + // access to the media section, if they don't they'll get auth errors. The entityResource + // acts differently in that it allows access if the user has access to any of the apps that + // might require it's use. Therefore we need to use the metaData property to get at the thumbnail + // value. + entityResource.getByIds(ids, 'Media').then(function (medias) { + // The service only returns item results for ids that exist (deleted items are silently ignored). + // This results in the picked items value to be set to contain only ids of picked items that could actually be found. + // Since a referenced item could potentially be restored later on, instead of changing the selected values here based + // on whether the items exist during a save event - we should keep "placeholder" items for picked items that currently + // could not be fetched. This will preserve references and ensure that the state of an item does not differ depending + // on whether it is simply resaved or not. + // This is done by remapping the int/guid ids into a new array of items, where we create "Deleted item" placeholders + // when there is no match for a selected id. This will ensure that the values being set on save, are the same as before. + medias = _.map(ids, function (id) { + var found = _.find(medias, function (m) { + // We could use coercion (two ='s) here .. but not sure if this works equally well in all browsers and + // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() + // compares and be completely sure it works. + return m.udi.toString() === id.toString() || m.id.toString() === id.toString(); + }); + if (found) { + return found; + } else { + return { + name: localizationService.dictionary.mediaPicker_deletedItem, + id: $scope.model.config.idType !== 'udi' ? id : null, + udi: $scope.model.config.idType === 'udi' ? id : null, + icon: 'icon-picture', + thumbnail: null, + trashed: true + }; + } + }); + _.each(medias, function (media, i) { + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + } + $scope.images.push(media); + if ($scope.model.config.idType === 'udi') { + $scope.ids.push(media.udi); + } else { + $scope.ids.push(media.id); + } + }); + $scope.sync(); + }); + } + } + setupViewModel(); + $scope.remove = function (index) { + $scope.images.splice(index, 1); + $scope.ids.splice(index, 1); + $scope.sync(); + }; + $scope.goToItem = function (item) { + $location.path('media/media/edit/' + item.id); + }; + $scope.add = function () { + $scope.mediaPickerOverlay = { + view: 'mediapicker', + title: 'Select media', + startNodeId: $scope.model.config.startNodeId, + startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, + multiPicker: multiPicker, + onlyImages: onlyImages, + disableFolderSelect: disableFolderSelect, + show: true, + submit: function (model) { + _.each(model.selectedImages, function (media, i) { + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + } + $scope.images.push(media); + if ($scope.model.config.idType === 'udi') { + $scope.ids.push(media.udi); + } else { + $scope.ids.push(media.id); + } + }); + $scope.sync(); + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + } + }; + }; + $scope.sortableOptions = { + update: function (e, ui) { + var r = []; + //TODO: Instead of doing this with a half second delay would be better to use a watch like we do in the + // content picker. THen we don't have to worry about setting ids, render models, models, we just set one and let the + // watch do all the rest. + $timeout(function () { + angular.forEach($scope.images, function (value, key) { + r.push($scope.model.config.idType === 'udi' ? value.udi : value.id); + }); + $scope.ids = r; + $scope.sync(); + }, 500, false); + } + }; + $scope.sync = function () { + $scope.model.value = $scope.ids.join(); + }; + $scope.showAdd = function () { + if (!multiPicker) { + if ($scope.model.value && $scope.model.value !== '') { + return false; + } + } + return true; + }; + //here we declare a special method which will be called whenever the value has changed from the server + //this is instead of doing a watch on the model.value = faster + $scope.model.onValueChanged = function (newVal, oldVal) { + //update the display val again if it has changed from the server + setupViewModel(); + }; + }); + //this controller simply tells the dialogs service to open a memberPicker window + //with a specified callback, this callback will receive an object with a selection on it + function memberGroupPicker($scope, dialogService) { + function trim(str, chr) { + var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); + return str.replace(rgxtrim, ''); + } + $scope.renderModel = []; + $scope.allowRemove = true; + if ($scope.model.value) { + var modelIds = $scope.model.value.split(','); + _.each(modelIds, function (item, i) { + $scope.renderModel.push({ + name: item, + id: item, + icon: 'icon-users' + }); + }); + } + $scope.openMemberGroupPicker = function () { + $scope.memberGroupPicker = {}; + $scope.memberGroupPicker.multiPicker = true; + $scope.memberGroupPicker.view = 'memberGroupPicker'; + $scope.memberGroupPicker.show = true; + $scope.memberGroupPicker.submit = function (model) { + if (model.selectedMemberGroups) { + _.each(model.selectedMemberGroups, function (item, i) { + $scope.add(item); + }); + } + if (model.selectedMemberGroup) { + $scope.clear(); + $scope.add(model.selectedMemberGroup); + } + $scope.memberGroupPicker.show = false; + $scope.memberGroupPicker = null; + }; + $scope.memberGroupPicker.close = function (oldModel) { + $scope.memberGroupPicker.show = false; + $scope.memberGroupPicker = null; + }; + }; + $scope.remove = function (index) { + $scope.renderModel.splice(index, 1); + }; + $scope.add = function (item) { + var currIds = _.map($scope.renderModel, function (i) { + return i.id; + }); + if (currIds.indexOf(item) < 0) { + $scope.renderModel.push({ + name: item, + id: item, + icon: 'icon-users' + }); + } + }; + $scope.clear = function () { + $scope.renderModel = []; + }; + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + var currIds = _.map($scope.renderModel, function (i) { + return i.id; + }); + $scope.model.value = trim(currIds.join(), ','); + }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.MemberGroupPickerController', memberGroupPicker); + function memberGroupController($rootScope, $scope, dialogService, mediaResource, imageHelper, $log) { + //set the available to the keys of the dictionary who's value is true + $scope.getAvailable = function () { + var available = []; + for (var n in $scope.model.value) { + if ($scope.model.value[n] === false) { + available.push(n); + } + } + return available; + }; + //set the selected to the keys of the dictionary who's value is true + $scope.getSelected = function () { + var selected = []; + for (var n in $scope.model.value) { + if ($scope.model.value[n] === true) { + selected.push(n); + } + } + return selected; + }; + $scope.addItem = function (item) { + //keep the model up to date + $scope.model.value[item] = true; + }; + $scope.removeItem = function (item) { + //keep the model up to date + $scope.model.value[item] = false; + }; + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.MemberGroupController', memberGroupController); + //this controller simply tells the dialogs service to open a memberPicker window + //with a specified callback, this callback will receive an object with a selection on it + function memberPickerController($scope, dialogService, entityResource, $log, iconHelper, angularHelper) { + function trim(str, chr) { + var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); + return str.replace(rgxtrim, ''); + } + $scope.renderModel = []; + $scope.allowRemove = true; + var dialogOptions = { + multiPicker: false, + entityType: 'Member', + section: 'member', + treeAlias: 'member', + filter: function (i) { + return i.metaData.isContainer == true; + }, + filterCssClass: 'not-allowed', + callback: function (data) { + if (angular.isArray(data)) { + _.each(data, function (item, i) { + $scope.add(item); + }); + } else { + $scope.clear(); + $scope.add(data); + } + angularHelper.getCurrentForm($scope).$setDirty(); + } + }; + //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the + // pre-value config on to the dialog options + if ($scope.model.config) { + angular.extend(dialogOptions, $scope.model.config); + } + $scope.openMemberPicker = function () { + $scope.memberPickerOverlay = dialogOptions; + $scope.memberPickerOverlay.view = 'memberPicker'; + $scope.memberPickerOverlay.show = true; + $scope.memberPickerOverlay.submit = function (model) { + if (model.selection) { + _.each(model.selection, function (item, i) { + $scope.add(item); + }); + } + $scope.memberPickerOverlay.show = false; + $scope.memberPickerOverlay = null; + }; + $scope.memberPickerOverlay.close = function (oldModel) { + $scope.memberPickerOverlay.show = false; + $scope.memberPickerOverlay = null; + }; + }; + $scope.remove = function (index) { + $scope.renderModel.splice(index, 1); + }; + $scope.add = function (item) { + var currIds = _.map($scope.renderModel, function (i) { + if ($scope.model.config.idType === 'udi') { + return i.udi; + } else { + return i.id; + } + }); + var itemId = $scope.model.config.idType === 'udi' ? item.udi : item.id; + if (currIds.indexOf(itemId) < 0) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.renderModel.push({ + name: item.name, + id: item.id, + udi: item.udi, + icon: item.icon + }); + } + }; + $scope.clear = function () { + $scope.renderModel = []; + }; + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + var currIds = _.map($scope.renderModel, function (i) { + if ($scope.model.config.idType === 'udi') { + return i.udi; + } else { + return i.id; + } + }); + $scope.model.value = trim(currIds.join(), ','); + }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + //load member data + var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; + entityResource.getByIds(modelIds, 'Member').then(function (data) { + _.each(data, function (item, i) { + // set default icon if it's missing + item.icon = item.icon ? iconHelper.convertFromLegacyIcon(item.icon) : 'icon-user'; + $scope.renderModel.push({ + name: item.name, + id: item.id, + udi: item.udi, + icon: item.icon + }); + }); + }); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.MemberPickerController', memberPickerController); + function MultipleTextBoxController($scope, $timeout) { + var backspaceHits = 0; + $scope.sortableOptions = { + axis: 'y', + containment: 'parent', + cursor: 'move', + items: '> div.control-group', + tolerance: 'pointer' + }; + if (!$scope.model.value) { + $scope.model.value = []; + } + //add any fields that there isn't values for + if ($scope.model.config.min > 0) { + for (var i = 0; i < $scope.model.config.min; i++) { + if (i + 1 > $scope.model.value.length) { + $scope.model.value.push({ value: '' }); + } + } + } + $scope.addRemoveOnKeyDown = function (event, index) { + var txtBoxValue = $scope.model.value[index]; + event.preventDefault(); + switch (event.keyCode) { + case 13: + if ($scope.model.config.max <= 0 && txtBoxValue.value || $scope.model.value.length < $scope.model.config.max && txtBoxValue.value) { + var newItemIndex = index + 1; + $scope.model.value.splice(newItemIndex, 0, { value: '' }); + //Focus on the newly added value + $scope.model.value[newItemIndex].hasFocus = true; + } + break; + case 8: + if ($scope.model.value.length > $scope.model.config.min) { + var remainder = []; + // Used to require an extra hit on backspace for the field to be removed + if (txtBoxValue.value === '') { + backspaceHits++; + } else { + backspaceHits = 0; + } + if (txtBoxValue.value === '' && backspaceHits === 2) { + for (var x = 0; x < $scope.model.value.length; x++) { + if (x !== index) { + remainder.push($scope.model.value[x]); + } + } + $scope.model.value = remainder; + var prevItemIndex = index - 1; + //Set focus back on false as the directive only watches for true + if (prevItemIndex >= 0) { + $scope.model.value[prevItemIndex].hasFocus = false; + $timeout(function () { + //Focus on the previous value + $scope.model.value[prevItemIndex].hasFocus = true; + }); + } + backspaceHits = 0; + } + } + break; + default: + } + }; + $scope.add = function () { + if ($scope.model.config.max <= 0 || $scope.model.value.length < $scope.model.config.max) { + $scope.model.value.push({ value: '' }); + // focus new value + var newItemIndex = $scope.model.value.length - 1; + $scope.model.value[newItemIndex].hasFocus = true; + } + }; + $scope.remove = function (index) { + var remainder = []; + for (var x = 0; x < $scope.model.value.length; x++) { + if (x !== index) { + remainder.push($scope.model.value[x]); + } + } + $scope.model.value = remainder; + }; + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.MultipleTextBoxController', MultipleTextBoxController); + angular.module('umbraco').controller('Umbraco.PropertyEditors.NestedContent.DocTypePickerController', [ + '$scope', + 'Umbraco.PropertyEditors.NestedContent.Resources', + function ($scope, ncResources) { + $scope.add = function () { + $scope.model.value.push({ + // As per PR #4, all stored content type aliases must be prefixed "nc" for easier recognition. + // For good measure we'll also prefix the tab alias "nc" + ncAlias: '', + ncTabAlias: '', + nameTemplate: '' + }); + }; + $scope.remove = function (index) { + $scope.model.value.splice(index, 1); + }; + $scope.sortableOptions = { + axis: 'y', + cursor: 'move', + handle: '.icon-navigation' + }; + $scope.docTypeTabs = {}; + ncResources.getContentTypes().then(function (docTypes) { + $scope.model.docTypes = docTypes; + // Populate document type tab dictionary + docTypes.forEach(function (value) { + $scope.docTypeTabs[value.alias] = value.tabs; + }); + }); + if (!$scope.model.value) { + $scope.model.value = []; + $scope.add(); + } + } + ]); + angular.module('umbraco').controller('Umbraco.PropertyEditors.NestedContent.PropertyEditorController', [ + '$scope', + '$interpolate', + '$filter', + '$timeout', + 'contentResource', + 'localizationService', + 'iconHelper', + function ($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper) { + //$scope.model.config.contentTypes; + //$scope.model.config.minItems; + //$scope.model.config.maxItems; + //console.log($scope); + var inited = false; + _.each($scope.model.config.contentTypes, function (contentType) { + contentType.nameExp = !!contentType.nameTemplate ? $interpolate(contentType.nameTemplate) : undefined; + }); + $scope.editIconTitle = ''; + $scope.moveIconTitle = ''; + $scope.deleteIconTitle = ''; + // localize the edit icon title + localizationService.localize('general_edit').then(function (value) { + $scope.editIconTitle = value; + }); + // localize the delete icon title + localizationService.localize('general_delete').then(function (value) { + $scope.deleteIconTitle = value; + }); + // localize the move icon title + localizationService.localize('actions_move').then(function (value) { + $scope.moveIconTitle = value; + }); + $scope.nodes = []; + $scope.currentNode = undefined; + $scope.realCurrentNode = undefined; + $scope.scaffolds = undefined; + $scope.sorting = false; + $scope.minItems = $scope.model.config.minItems || 0; + $scope.maxItems = $scope.model.config.maxItems || 0; + if ($scope.maxItems == 0) + $scope.maxItems = 1000; + $scope.singleMode = $scope.minItems == 1 && $scope.maxItems == 1; + $scope.showIcons = $scope.model.config.showIcons || true; + $scope.wideMode = $scope.model.config.hideLabel == '1'; + $scope.overlayMenu = { + show: false, + style: {} + }; + // helper to force the current form into the dirty state + $scope.setDirty = function () { + if ($scope.propertyForm) { + $scope.propertyForm.$setDirty(); + } + }; + $scope.addNode = function (alias) { + var scaffold = $scope.getScaffold(alias); + var newNode = initNode(scaffold, null); + $scope.currentNode = newNode; + $scope.setDirty(); + $scope.closeNodeTypePicker(); + }; + $scope.openNodeTypePicker = function (event) { + if ($scope.nodes.length >= $scope.maxItems) { + return; + } + // this could be used for future limiting on node types + $scope.overlayMenu.scaffolds = []; + _.each($scope.scaffolds, function (scaffold) { + $scope.overlayMenu.scaffolds.push({ + alias: scaffold.contentTypeAlias, + name: scaffold.contentTypeName, + icon: iconHelper.convertFromLegacyIcon(scaffold.icon) + }); + }); + if ($scope.overlayMenu.scaffolds.length == 0) { + return; + } + if ($scope.overlayMenu.scaffolds.length == 1) { + // only one scaffold type - no need to display the picker + $scope.addNode($scope.scaffolds[0].contentTypeAlias); + return; + } + $scope.overlayMenu.show = true; + }; + $scope.closeNodeTypePicker = function () { + $scope.overlayMenu.show = false; + }; + $scope.editNode = function (idx) { + if ($scope.currentNode && $scope.currentNode.key == $scope.nodes[idx].key) { + $scope.currentNode = undefined; + } else { + $scope.currentNode = $scope.nodes[idx]; + } + }; + $scope.deleteNode = function (idx) { + if ($scope.nodes.length > $scope.model.config.minItems) { + if ($scope.model.config.confirmDeletes && $scope.model.config.confirmDeletes == 1) { + localizationService.localize('content_nestedContentDeleteItem').then(function (value) { + if (confirm(value)) { + $scope.nodes.splice(idx, 1); + $scope.setDirty(); + updateModel(); + } + }); + } else { + $scope.nodes.splice(idx, 1); + $scope.setDirty(); + updateModel(); + } + } + }; + $scope.getName = function (idx) { + var name = 'Item ' + (idx + 1); + if ($scope.model.value[idx]) { + var contentType = $scope.getContentTypeConfig($scope.model.value[idx].ncContentTypeAlias); + if (contentType != null && contentType.nameExp) { + // Run the expression against the stored dictionary value, NOT the node object + var item = $scope.model.value[idx]; + // Add a temporary index property + item['$index'] = idx + 1; + var newName = contentType.nameExp(item); + if (newName && (newName = $.trim(newName))) { + name = newName; + } + // Delete the index property as we don't want to persist it + delete item['$index']; + } + } + // Update the nodes actual name value + if ($scope.nodes[idx].name !== name) { + $scope.nodes[idx].name = name; + } + return name; + }; + $scope.getIcon = function (idx) { + var scaffold = $scope.getScaffold($scope.model.value[idx].ncContentTypeAlias); + return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : 'icon-folder'; + }; + $scope.sortableOptions = { + axis: 'y', + cursor: 'move', + handle: '.nested-content__icon--move', + start: function (ev, ui) { + // Yea, yea, we shouldn't modify the dom, sue me + $('#nested-content--' + $scope.model.id + ' .umb-rte textarea').each(function () { + tinymce.execCommand('mceRemoveEditor', false, $(this).attr('id')); + $(this).css('visibility', 'hidden'); + }); + $scope.$apply(function () { + $scope.sorting = true; + }); + }, + update: function (ev, ui) { + $scope.setDirty(); + }, + stop: function (ev, ui) { + $('#nested-content--' + $scope.model.id + ' .umb-rte textarea').each(function () { + tinymce.execCommand('mceAddEditor', true, $(this).attr('id')); + $(this).css('visibility', 'visible'); + }); + $scope.$apply(function () { + $scope.sorting = false; + updateModel(); + }); + } + }; + $scope.getScaffold = function (alias) { + return _.find($scope.scaffolds, function (scaffold) { + return scaffold.contentTypeAlias == alias; + }); + }; + $scope.getContentTypeConfig = function (alias) { + return _.find($scope.model.config.contentTypes, function (contentType) { + return contentType.ncAlias == alias; + }); + }; + var notSupported = [ + 'Umbraco.CheckBoxList', + 'Umbraco.DropDownMultiple', + 'Umbraco.MacroContainer', + 'Umbraco.RadioButtonList', + 'Umbraco.MultipleTextstring', + 'Umbraco.Tags', + 'Umbraco.UploadField', + 'Umbraco.ImageCropper' + ]; + // Initialize + var scaffoldsLoaded = 0; + $scope.scaffolds = []; + _.each($scope.model.config.contentTypes, function (contentType) { + contentResource.getScaffold(-20, contentType.ncAlias).then(function (scaffold) { + // remove all tabs except the specified tab + var tab = _.find(scaffold.tabs, function (tab) { + return tab.id != 0 && (tab.alias.toLowerCase() == contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias == ''); + }); + scaffold.tabs = []; + if (tab) { + scaffold.tabs.push(tab); + angular.forEach(tab.properties, function (property) { + if (_.find(notSupported, function (x) { + return x === property.editor; + })) { + property.notSupported = true; + //TODO: Not supported message to be replaced with 'content_nestedContentEditorNotSupported' dictionary key. Currently not possible due to async/timing quirk. + property.notSupportedMessage = 'Property ' + property.label + ' uses editor ' + property.editor + ' which is not supported by Nested Content.'; + } + }); + } + // Store the scaffold object + $scope.scaffolds.push(scaffold); + scaffoldsLoaded++; + initIfAllScaffoldsHaveLoaded(); + }, function (error) { + scaffoldsLoaded++; + initIfAllScaffoldsHaveLoaded(); + }); + }); + var initIfAllScaffoldsHaveLoaded = function () { + // Initialize when all scaffolds have loaded + if ($scope.model.config.contentTypes.length == scaffoldsLoaded) { + // Because we're loading the scaffolds async one at a time, we need to + // sort them explicitly according to the sort order defined by the data type. + var contentTypeAliases = []; + _.each($scope.model.config.contentTypes, function (contentType) { + contentTypeAliases.push(contentType.ncAlias); + }); + $scope.scaffolds = $filter('orderBy')($scope.scaffolds, function (s) { + return contentTypeAliases.indexOf(s.contentTypeAlias); + }); + // Convert stored nodes + if ($scope.model.value) { + for (var i = 0; i < $scope.model.value.length; i++) { + var item = $scope.model.value[i]; + var scaffold = $scope.getScaffold(item.ncContentTypeAlias); + if (scaffold == null) { + // No such scaffold - the content type might have been deleted. We need to skip it. + continue; + } + initNode(scaffold, item); + } + } + // Enforce min items + if ($scope.nodes.length < $scope.model.config.minItems) { + for (var i = $scope.nodes.length; i < $scope.model.config.minItems; i++) { + $scope.addNode($scope.scaffolds[0].contentTypeAlias); + } + } + // If there is only one item, set it as current node + if ($scope.singleMode || $scope.nodes.length == 1 && $scope.maxItems == 1) { + $scope.currentNode = $scope.nodes[0]; + } + inited = true; + } + }; + var initNode = function (scaffold, item) { + var node = angular.copy(scaffold); + node.key = item && item.key ? item.key : UUID.generate(); + node.ncContentTypeAlias = scaffold.contentTypeAlias; + for (var t = 0; t < node.tabs.length; t++) { + var tab = node.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + prop.propertyAlias = prop.alias; + prop.alias = $scope.model.alias + '___' + prop.alias; + // Force validation to occur server side as this is the + // only way we can have consistancy between mandatory and + // regex validation messages. Not ideal, but it works. + prop.validation = { + mandatory: false, + pattern: '' + }; + if (item) { + if (item[prop.propertyAlias]) { + prop.value = item[prop.propertyAlias]; + } + } + } + } + $scope.nodes.push(node); + return node; + }; + var updateModel = function () { + if ($scope.realCurrentNode) { + $scope.$broadcast('ncSyncVal', { key: $scope.realCurrentNode.key }); + } + if (inited) { + var newValues = []; + for (var i = 0; i < $scope.nodes.length; i++) { + var node = $scope.nodes[i]; + var newValue = { + key: node.key, + name: node.name, + ncContentTypeAlias: node.ncContentTypeAlias + }; + for (var t = 0; t < node.tabs.length; t++) { + var tab = node.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (typeof prop.value !== 'function') { + newValue[prop.propertyAlias] = prop.value; + } + } + } + newValues.push(newValue); + } + $scope.model.value = newValues; + } + }; + $scope.$watch('currentNode', function (newVal) { + updateModel(); + $scope.realCurrentNode = newVal; + }); + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + updateModel(); + }); + $scope.$on('$destroy', function () { + unsubscribe(); + }); + //TODO: Move this into a shared location? + var UUID = function () { + var self = {}; + var lut = []; + for (var i = 0; i < 256; i++) { + lut[i] = (i < 16 ? '0' : '') + i.toString(16); + } + self.generate = function () { + var d0 = Math.random() * 4294967295 | 0; + var d1 = Math.random() * 4294967295 | 0; + var d2 = Math.random() * 4294967295 | 0; + var d3 = Math.random() * 4294967295 | 0; + return lut[d0 & 255] + lut[d0 >> 8 & 255] + lut[d0 >> 16 & 255] + lut[d0 >> 24 & 255] + '-' + lut[d1 & 255] + lut[d1 >> 8 & 255] + '-' + lut[d1 >> 16 & 15 | 64] + lut[d1 >> 24 & 255] + '-' + lut[d2 & 63 | 128] + lut[d2 >> 8 & 255] + '-' + lut[d2 >> 16 & 255] + lut[d2 >> 24 & 255] + lut[d3 & 255] + lut[d3 >> 8 & 255] + lut[d3 >> 16 & 255] + lut[d3 >> 24 & 255]; + }; + return self; + }(); + } + ]); + angular.module('umbraco').controller('Umbraco.PropertyEditors.RadioButtonsController', function ($scope) { + if (angular.isObject($scope.model.config.items)) { + //now we need to format the items in the dictionary because we always want to have an array + var newItems = []; + var vals = _.values($scope.model.config.items); + var keys = _.keys($scope.model.config.items); + for (var i = 0; i < vals.length; i++) { + newItems.push({ + id: keys[i], + sortOrder: vals[i].sortOrder, + value: vals[i].value + }); + } + //ensure the items are sorted by the provided sort order + newItems.sort(function (a, b) { + return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; + }); + //re-assign + $scope.model.config.items = newItems; + } + }); + /** + * @ngdoc controller + * @name Umbraco.Editors.ReadOnlyValueController + * @function + * + * @description + * The controller for the readonlyvalue property editor. + * This controller offer more functionality than just a simple label as it will be able to apply formatting to the + * value to be displayed. This means that we also have to apply more complex logic of watching the model value when + * it changes because we are creating a new scope value called displayvalue which will never change based on the server data. + * In some cases after a form submission, the server will modify the data that has been persisted, especially in the cases of + * readonlyvalues so we need to ensure that after the form is submitted that the new data is reflected here. +*/ + function ReadOnlyValueController($rootScope, $scope, $filter) { + function formatDisplayValue() { + if ($scope.model.config && angular.isArray($scope.model.config) && $scope.model.config.length > 0 && $scope.model.config[0] && $scope.model.config.filter) { + if ($scope.model.config.format) { + $scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value, $scope.model.config.format); + } else { + $scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value); + } + } else { + $scope.displayvalue = $scope.model.value; + } + } + //format the display value on init: + formatDisplayValue(); + $scope.$watch('model.value', function (newVal, oldVal) { + //cannot just check for !newVal because it might be an empty string which we + //want to look for. + if (newVal !== null && newVal !== undefined && newVal !== oldVal) { + //update the display val again + formatDisplayValue(); + } + }); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.ReadOnlyValueController', ReadOnlyValueController); + angular.module('umbraco').controller('Umbraco.PropertyEditors.RelatedLinksController', function ($rootScope, $scope, dialogService, iconHelper) { + if (!$scope.model.value) { + $scope.model.value = []; + } + $scope.model.config.max = isNumeric($scope.model.config.max) && $scope.model.config.max !== 0 ? $scope.model.config.max : Number.MAX_VALUE; + $scope.newCaption = ''; + $scope.newLink = 'http://'; + $scope.newNewWindow = false; + $scope.newInternal = null; + $scope.newInternalName = ''; + $scope.newInternalIcon = null; + $scope.addExternal = true; + $scope.currentEditLink = null; + $scope.hasError = false; + $scope.internal = function ($event) { + $scope.currentEditLink = null; + $scope.contentPickerOverlay = {}; + $scope.contentPickerOverlay.view = 'contentpicker'; + $scope.contentPickerOverlay.multiPicker = false; + $scope.contentPickerOverlay.show = true; + $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : 'int'; + $scope.contentPickerOverlay.submit = function (model) { + select(model.selection[0]); + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + $scope.contentPickerOverlay.close = function (oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + $event.preventDefault(); + }; + $scope.selectInternal = function ($event, link) { + $scope.currentEditLink = link; + $scope.contentPickerOverlay = {}; + $scope.contentPickerOverlay.view = 'contentpicker'; + $scope.contentPickerOverlay.multiPicker = false; + $scope.contentPickerOverlay.show = true; + $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : 'int'; + $scope.contentPickerOverlay.submit = function (model) { + select(model.selection[0]); + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + $scope.contentPickerOverlay.close = function (oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + $event.preventDefault(); + }; + $scope.edit = function (idx) { + for (var i = 0; i < $scope.model.value.length; i++) { + $scope.model.value[i].edit = false; + } + $scope.model.value[idx].edit = true; + }; + $scope.saveEdit = function (idx) { + $scope.model.value[idx].title = $scope.model.value[idx].caption; + $scope.model.value[idx].edit = false; + }; + $scope.delete = function (idx) { + $scope.model.value.splice(idx, 1); + }; + $scope.add = function ($event) { + if (!angular.isArray($scope.model.value)) { + $scope.model.value = []; + } + if ($scope.newCaption == '') { + $scope.hasError = true; + } else { + if ($scope.addExternal) { + var newExtLink = new function () { + this.caption = $scope.newCaption; + this.link = $scope.newLink; + this.newWindow = $scope.newNewWindow; + this.edit = false; + this.isInternal = false; + this.type = 'external'; + this.title = $scope.newCaption; + }(); + $scope.model.value.push(newExtLink); + } else { + var newIntLink = new function () { + this.caption = $scope.newCaption; + this.link = $scope.newInternal; + this.newWindow = $scope.newNewWindow; + this.internal = $scope.newInternal; + this.edit = false; + this.isInternal = true; + this.internalName = $scope.newInternalName; + this.internalIcon = $scope.newInternalIcon; + this.type = 'internal'; + this.title = $scope.newCaption; + }(); + $scope.model.value.push(newIntLink); + } + $scope.newCaption = ''; + $scope.newLink = 'http://'; + $scope.newNewWindow = false; + $scope.newInternal = null; + $scope.newInternalName = ''; + $scope.newInternalIcon = null; + } + $event.preventDefault(); + }; + $scope.switch = function ($event) { + $scope.addExternal = !$scope.addExternal; + $event.preventDefault(); + }; + $scope.switchLinkType = function ($event, link) { + link.isInternal = !link.isInternal; + link.type = link.isInternal ? 'internal' : 'external'; + if (!link.isInternal) + link.link = $scope.newLink; + $event.preventDefault(); + }; + $scope.move = function (index, direction) { + var temp = $scope.model.value[index]; + $scope.model.value[index] = $scope.model.value[index + direction]; + $scope.model.value[index + direction] = temp; + }; + //helper for determining if a user can add items + $scope.canAdd = function () { + return $scope.model.config.max <= 0 || $scope.model.config.max > countVisible(); + }; + //helper that returns if an item can be sorted + $scope.canSort = function () { + return countVisible() > 1; + }; + $scope.sortableOptions = { + axis: 'y', + handle: '.handle', + cursor: 'move', + cancel: '.no-drag', + containment: 'parent', + placeholder: 'sortable-placeholder', + forcePlaceholderSize: true, + helper: function (e, ui) { + // When sorting table rows, the cells collapse. This helper fixes that: http://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/ + ui.children().each(function () { + $(this).width($(this).width()); + }); + return ui; + }, + items: '> tr:not(.unsortable)', + tolerance: 'pointer', + update: function (e, ui) { + // Get the new and old index for the moved element (using the URL as the identifier) + var newIndex = ui.item.index(); + var movedLinkUrl = ui.item.attr('data-link'); + var originalIndex = getElementIndexByUrl(movedLinkUrl); + // Move the element in the model + var movedElement = $scope.model.value[originalIndex]; + $scope.model.value.splice(originalIndex, 1); + $scope.model.value.splice(newIndex, 0, movedElement); + }, + start: function (e, ui) { + //ui.placeholder.html(""); + // Build a placeholder cell that spans all the cells in the row: http://stackoverflow.com/questions/25845310/jquery-ui-sortable-and-table-cell-size + var cellCount = 0; + $('td, th', ui.helper).each(function () { + // For each td or th try and get it's colspan attribute, and add that or 1 to the total + var colspan = 1; + var colspanAttr = $(this).attr('colspan'); + if (colspanAttr > 1) { + colspan = colspanAttr; + } + cellCount += colspan; + }); + // Add the placeholder UI - note that this is the item's content, so td rather than tr - and set height of tr + ui.placeholder.html('').height(ui.item.height()); + } + }; + //helper to count what is visible + function countVisible() { + return $scope.model.value.length; + } + function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + function getElementIndexByUrl(url) { + for (var i = 0; i < $scope.model.value.length; i++) { + if ($scope.model.value[i].link == url) { + return i; + } + } + return -1; + } + function select(data) { + if ($scope.currentEditLink != null) { + $scope.currentEditLink.internal = $scope.model.config.idType === 'udi' ? data.udi : data.id; + $scope.currentEditLink.internalName = data.name; + $scope.currentEditLink.internalIcon = iconHelper.convertFromLegacyIcon(data.icon); + $scope.currentEditLink.link = $scope.model.config.idType === 'udi' ? data.udi : data.id; + } else { + $scope.newInternal = $scope.model.config.idType === 'udi' ? data.udi : data.id; + $scope.newInternalName = data.name; + $scope.newInternalIcon = iconHelper.convertFromLegacyIcon(data.icon); + } + } + }); + angular.module('umbraco').controller('Umbraco.PropertyEditors.RTEController', function ($rootScope, $scope, $q, $locale, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource, macroService) { + $scope.isLoading = true; + //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias + // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because + // we have this mini content editor panel that can be launched with MNTP. + var d = new Date(); + var n = d.getTime(); + $scope.textAreaHtmlId = $scope.model.alias + '_' + n + '_rte'; + var alreadyDirty = false; + function syncContent(editor) { + editor.save(); + angularHelper.safeApply($scope, function () { + $scope.model.value = editor.getContent(); + }); + if (!alreadyDirty) { + //make the form dirty manually so that the track changes works, setting our model doesn't trigger + // the angular bits because tinymce replaces the textarea. + var currForm = angularHelper.getCurrentForm($scope); + currForm.$setDirty(); + alreadyDirty = true; + } + } + tinyMceService.configuration().then(function (tinyMceConfig) { + //config value from general tinymce.config file + var validElements = tinyMceConfig.validElements; + //These are absolutely required in order for the macros to render inline + //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce + var extendedValidElements = '@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],span[id|class|style]'; + var invalidElements = tinyMceConfig.inValidElements; + var plugins = _.map(tinyMceConfig.plugins, function (plugin) { + if (plugin.useOnFrontend) { + return plugin.name; + } + }).join(' '); + var editorConfig = $scope.model.config.editor; + if (!editorConfig || angular.isString(editorConfig)) { + editorConfig = tinyMceService.defaultPrevalues(); + } + //config value on the data type + var toolbar = editorConfig.toolbar.join(' | '); + var stylesheets = []; + var styleFormats = []; + var await = []; + if (!editorConfig.maxImageSize && editorConfig.maxImageSize != 0) { + editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize; + } + //queue file loading + if (typeof tinymce === 'undefined') { + // Don't reload tinymce if already loaded + await.push(assetsService.loadJs('lib/tinymce/tinymce.min.js', $scope)); + } + //queue rules loading + angular.forEach(editorConfig.stylesheets, function (val, key) { + stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + '/' + val + '.css?' + new Date().getTime()); + await.push(stylesheetResource.getRulesByName(val).then(function (rules) { + angular.forEach(rules, function (rule) { + var r = {}; + r.title = rule.name; + if (rule.selector[0] == '.') { + r.inline = 'span'; + r.classes = rule.selector.substring(1); + } else if (rule.selector[0] == '#') { + r.inline = 'span'; + r.attributes = { id: rule.selector.substring(1) }; + } else if (rule.selector[0] != '.' && rule.selector.indexOf('.') > -1) { + var split = rule.selector.split('.'); + r.block = split[0]; + r.classes = rule.selector.substring(rule.selector.indexOf('.') + 1).replace('.', ' '); + } else if (rule.selector[0] != '#' && rule.selector.indexOf('#') > -1) { + var split = rule.selector.split('#'); + r.block = split[0]; + r.classes = rule.selector.substring(rule.selector.indexOf('#') + 1); + } else { + r.block = rule.selector; + } + styleFormats.push(r); + }); + })); + }); + //stores a reference to the editor + var tinyMceEditor = null; + // these languages are available for localization + var availableLanguages = [ + 'da', + 'de', + 'en', + 'en_us', + 'fi', + 'fr', + 'he', + 'it', + 'ja', + 'nl', + 'no', + 'pl', + 'pt', + 'ru', + 'sv', + 'zh' + ]; + //define fallback language + var language = 'en_us'; + //get locale from angular and match tinymce format. Angular localization is always in the format of ru-ru, de-de, en-gb, etc. + //wheras tinymce is in the format of ru, de, en, en_us, etc. + var localeId = $locale.id.replace('-', '_'); + //try matching the language using full locale format + var languageMatch = _.find(availableLanguages, function (o) { + return o === localeId; + }); + //if no matches, try matching using only the language + if (languageMatch === undefined) { + var localeParts = localeId.split('_'); + languageMatch = _.find(availableLanguages, function (o) { + return o === localeParts[0]; + }); + } + //if a match was found - set the language + if (languageMatch !== undefined) { + language = languageMatch; + } + //wait for queue to end + $q.all(await).then(function () { + //create a baseline Config to exten upon + var baseLineConfigObj = { + mode: 'exact', + skin: 'umbraco', + plugins: plugins, + valid_elements: validElements, + invalid_elements: invalidElements, + extended_valid_elements: extendedValidElements, + menubar: false, + statusbar: false, + height: editorConfig.dimensions.height, + width: editorConfig.dimensions.width, + maxImageSize: editorConfig.maxImageSize, + toolbar: toolbar, + content_css: stylesheets, + relative_urls: false, + style_formats: styleFormats, + language: language + }; + if (tinyMceConfig.customConfig) { + //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to + // convert it to json instead of having it as a string since this is what tinymce requires + for (var i in tinyMceConfig.customConfig) { + var val = tinyMceConfig.customConfig[i]; + if (val) { + val = val.toString().trim(); + if (val.detectIsJson()) { + try { + tinyMceConfig.customConfig[i] = JSON.parse(val); + //now we need to check if this custom config key is defined in our baseline, if it is we don't want to + //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise + //if it's an object it will overwrite the baseline + if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { + //concat it and below this concat'd array will overwrite the baseline in angular.extend + tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]); + } + } catch (e) { + } + } + } + } + angular.extend(baseLineConfigObj, tinyMceConfig.customConfig); + } + //set all the things that user configs should not be able to override + baseLineConfigObj.elements = $scope.textAreaHtmlId; + //this is the exact textarea id to replace! + baseLineConfigObj.setup = function (editor) { + //set the reference + tinyMceEditor = editor; + //enable browser based spell checking + editor.on('init', function (e) { + editor.getBody().setAttribute('spellcheck', true); + }); + //We need to listen on multiple things here because of the nature of tinymce, it doesn't + //fire events when you think! + //The change event doesn't fire when content changes, only when cursor points are changed and undo points + //are created. the blur event doesn't fire if you insert content into the editor with a button and then + //press save. + //We have a couple of options, one is to do a set timeout and check for isDirty on the editor, or we can + //listen to both change and blur and also on our own 'saving' event. I think this will be best because a + //timer might end up using unwanted cpu and we'd still have to listen to our saving event in case they clicked + //save before the timeout elapsed. + //TODO: We need to re-enable something like this to ensure the track changes is working with tinymce + // so we can detect if the form is dirty or not, Per has some better events to use as this one triggers + // even if you just enter/exit with mouse cursuor which doesn't really mean it's changed. + // see: http://issues.umbraco.org/issue/U4-4485 + //var alreadyDirty = false; + //editor.on('change', function (e) { + // angularHelper.safeApply($scope, function () { + // $scope.model.value = editor.getContent(); + // if (!alreadyDirty) { + // //make the form dirty manually so that the track changes works, setting our model doesn't trigger + // // the angular bits because tinymce replaces the textarea. + // var currForm = angularHelper.getCurrentForm($scope); + // currForm.$setDirty(); + // alreadyDirty = true; + // } + // }); + //}); + //when we leave the editor (maybe) + editor.on('blur', function (e) { + editor.save(); + angularHelper.safeApply($scope, function () { + $scope.model.value = editor.getContent(); + }); + }); + //when buttons modify content + editor.on('ExecCommand', function (e) { + syncContent(editor); + }); + // Update model on keypress + editor.on('KeyUp', function (e) { + syncContent(editor); + }); + // Update model on change, i.e. copy/pasted text, plugins altering content + editor.on('SetContent', function (e) { + if (!e.initial) { + syncContent(editor); + } + }); + editor.on('ObjectResized', function (e) { + var qs = '?width=' + e.width + '&height=' + e.height; + var srcAttr = $(e.target).attr('src'); + var path = srcAttr.split('?')[0]; + $(e.target).attr('data-mce-src', path + qs); + syncContent(editor); + }); + tinyMceService.createLinkPicker(editor, $scope, function (currentTarget, anchorElement) { + $scope.linkPickerOverlay = { + view: 'linkpicker', + currentTarget: currentTarget, + show: true, + submit: function (model) { + tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); + $scope.linkPickerOverlay.show = false; + $scope.linkPickerOverlay = null; + } + }; + }); + //Create the insert media plugin + tinyMceService.createMediaPicker(editor, $scope, function (currentTarget, userData) { + $scope.mediaPickerOverlay = { + currentTarget: currentTarget, + onlyImages: true, + showDetails: true, + disableFolderSelect: true, + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], + startNodeIsVirtual: userData.startMediaIds.length !== 1, + view: 'mediapicker', + show: true, + submit: function (model) { + tinyMceService.insertMediaInEditor(editor, model.selectedImages[0]); + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + } + }; + }); + //Create the embedded plugin + tinyMceService.createInsertEmbeddedMedia(editor, $scope, function () { + $scope.embedOverlay = { + view: 'embed', + show: true, + submit: function (model) { + tinyMceService.insertEmbeddedMediaInEditor(editor, model.embed.preview); + $scope.embedOverlay.show = false; + $scope.embedOverlay = null; + } + }; + }); + //Create the insert macro plugin + tinyMceService.createInsertMacro(editor, $scope, function (dialogData) { + $scope.macroPickerOverlay = { + view: 'macropicker', + dialogData: dialogData, + show: true, + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + tinyMceService.insertMacroInEditor(editor, macroObject, $scope); + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + } + }; + }); + }; + /** Loads in the editor */ + function loadTinyMce() { + //we need to add a timeout here, to force a redraw so TinyMCE can find + //the elements needed + $timeout(function () { + tinymce.DOM.events.domLoaded = true; + tinymce.init(baseLineConfigObj); + $scope.isLoading = false; + }, 200, false); + } + loadTinyMce(); + //here we declare a special method which will be called whenever the value has changed from the server + //this is instead of doing a watch on the model.value = faster + $scope.model.onValueChanged = function (newVal, oldVal) { + //update the display val again if it has changed from the server; + tinyMceEditor.setContent(newVal, { format: 'raw' }); + //we need to manually fire this event since it is only ever fired based on loading from the DOM, this + // is required for our plugins listening to this event to execute + tinyMceEditor.fire('LoadContent', null); + }; + //listen for formSubmitting event (the result is callback used to remove the event subscription) + var unsubscribe = $scope.$on('formSubmitting', function () { + //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer + // we do parse it out on the server side but would be nice to do that on the client side before as well. + $scope.model.value = tinyMceEditor ? tinyMceEditor.getContent() : null; + }); + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom + // element might still be there even after the modal has been hidden. + $scope.$on('$destroy', function () { + unsubscribe(); + }); + }); + }); + }); + angular.module('umbraco').controller('Umbraco.PrevalueEditors.RteController', function ($scope, $timeout, $log, tinyMceService, stylesheetResource, assetsService) { + var cfg = tinyMceService.defaultPrevalues(); + if ($scope.model.value) { + if (angular.isString($scope.model.value)) { + $scope.model.value = cfg; + } + } else { + $scope.model.value = cfg; + } + if (!$scope.model.value.stylesheets) { + $scope.model.value.stylesheets = []; + } + if (!$scope.model.value.toolbar) { + $scope.model.value.toolbar = []; + } + if (!$scope.model.value.maxImageSize && $scope.model.value.maxImageSize != 0) { + $scope.model.value.maxImageSize = cfg.maxImageSize; + } + tinyMceService.configuration().then(function (config) { + $scope.tinyMceConfig = config; + // extend commands with properties for font-icon and if it is a custom command + $scope.tinyMceConfig.commands = _.map($scope.tinyMceConfig.commands, function (obj) { + var icon = getFontIcon(obj.frontEndCommand); + return angular.extend(obj, { + fontIcon: icon.name, + isCustom: icon.isCustom + }); + }); + }); + stylesheetResource.getAll().then(function (stylesheets) { + $scope.stylesheets = stylesheets; + }); + $scope.selected = function (cmd, alias, lookup) { + if (lookup && angular.isArray(lookup)) { + cmd.selected = lookup.indexOf(alias) >= 0; + return cmd.selected; + } + return false; + }; + $scope.selectCommand = function (command) { + var index = $scope.model.value.toolbar.indexOf(command.frontEndCommand); + if (command.selected && index === -1) { + $scope.model.value.toolbar.push(command.frontEndCommand); + } else if (index >= 0) { + $scope.model.value.toolbar.splice(index, 1); + } + }; + $scope.selectStylesheet = function (css) { + var index = $scope.model.value.stylesheets.indexOf(css.name); + if (css.selected && index === -1) { + $scope.model.value.stylesheets.push(css.name); + } else if (index >= 0) { + $scope.model.value.stylesheets.splice(index, 1); + } + }; + // map properties for specific commands + function getFontIcon(alias) { + var icon = { + name: alias, + isCustom: false + }; + switch (alias) { + case 'codemirror': + icon.name = 'code'; + icon.isCustom = false; + break; + case 'styleselect': + case 'fontsizeselect': + icon.name = 'icon-list'; + icon.isCustom = true; + break; + case 'umbembeddialog': + icon.name = 'icon-tv'; + icon.isCustom = true; + break; + case 'umbmediapicker': + icon.name = 'icon-picture'; + icon.isCustom = true; + break; + case 'umbmacro': + icon.name = 'icon-settings-alt'; + icon.isCustom = true; + break; + case 'umbmacro': + icon.name = 'icon-settings-alt'; + icon.isCustom = true; + break; + default: + icon.name = alias; + icon.isCustom = false; + } + return icon; + } + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + var commands = _.where($scope.tinyMceConfig.commands, { selected: true }); + $scope.model.value.toolbar = _.pluck(commands, 'frontEndCommand'); + }); + // when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + // load TinyMCE skin which contains css for font-icons + assetsService.loadCss('lib/tinymce/skins/umbraco/skin.min.css'); + }); + function sliderController($scope, $log, $element, assetsService, angularHelper) { + //configure some defaults + if (!$scope.model.config.orientation) { + $scope.model.config.orientation = 'horizontal'; + } + if (!$scope.model.config.enableRange) { + $scope.model.config.enableRange = false; + } else { + $scope.model.config.enableRange = $scope.model.config.enableRange === '1' ? true : false; + } + if (!$scope.model.config.initVal1) { + $scope.model.config.initVal1 = 0; + } else { + $scope.model.config.initVal1 = parseFloat($scope.model.config.initVal1); + } + if (!$scope.model.config.initVal2) { + $scope.model.config.initVal2 = 0; + } else { + $scope.model.config.initVal2 = parseFloat($scope.model.config.initVal2); + } + if (!$scope.model.config.minVal) { + $scope.model.config.minVal = 0; + } else { + $scope.model.config.minVal = parseFloat($scope.model.config.minVal); + } + if (!$scope.model.config.maxVal) { + $scope.model.config.maxVal = 100; + } else { + $scope.model.config.maxVal = parseFloat($scope.model.config.maxVal); + } + if (!$scope.model.config.step) { + $scope.model.config.step = 1; + } else { + $scope.model.config.step = parseFloat($scope.model.config.step); + } + if (!$scope.model.config.handle) { + $scope.model.config.handle = 'round'; + } + if (!$scope.model.config.reversed) { + $scope.model.config.reversed = false; + } else { + $scope.model.config.reversed = $scope.model.config.reversed === '1' ? true : false; + } + if (!$scope.model.config.tooltip) { + $scope.model.config.tooltip = 'show'; + } + if (!$scope.model.config.tooltipSplit) { + $scope.model.config.tooltipSplit = false; + } else { + $scope.model.config.tooltipSplit = $scope.model.config.tooltipSplit === '1' ? true : false; + } + if ($scope.model.config.tooltipFormat) { + $scope.model.config.formatter = function (value) { + if (angular.isArray(value) && $scope.model.config.enableRange) { + return $scope.model.config.tooltipFormat.replace('{0}', value[0]).replace('{1}', value[1]); + } else { + return $scope.model.config.tooltipFormat.replace('{0}', value); + } + }; + } + if (!$scope.model.config.ticks) { + $scope.model.config.ticks = []; + } else { + // returns comma-separated string to an array, e.g. [0, 100, 200, 300, 400] + $scope.model.config.ticks = _.map($scope.model.config.ticks.split(','), function (item) { + return parseInt(item.trim()); + }); + } + if (!$scope.model.config.ticksPositions) { + $scope.model.config.ticksPositions = []; + } else { + // returns comma-separated string to an array, e.g. [0, 30, 60, 70, 90, 100] + $scope.model.config.ticksPositions = _.map($scope.model.config.ticksPositions.split(','), function (item) { + return parseInt(item.trim()); + }); + console.log($scope.model.config.ticksPositions); + } + if (!$scope.model.config.ticksLabels) { + $scope.model.config.ticksLabels = []; + } else { + // returns comma-separated string to an array, e.g. ['$0', '$100', '$200', '$300', '$400'] + $scope.model.config.ticksLabels = _.map($scope.model.config.ticksLabels.split(','), function (item) { + return item.trim(); + }); + } + if (!$scope.model.config.ticksSnapBounds) { + $scope.model.config.ticksSnapBounds = 0; + } else { + $scope.model.config.ticksSnapBounds = parseFloat($scope.model.config.ticksSnapBounds); + } + /** This creates the slider with the model values - it's called on startup and if the model value changes */ + function createSlider() { + //the value that we'll give the slider - if it's a range, we store our value as a comma separated val but this slider expects an array + var sliderVal = null; + //configure the model value based on if range is enabled or not + if ($scope.model.config.enableRange == true) { + //If no value saved yet - then use default value + //If it contains a single value - then also create a new array value + if (!$scope.model.value || $scope.model.value.indexOf(',') == -1) { + var i1 = parseFloat($scope.model.config.initVal1); + var i2 = parseFloat($scope.model.config.initVal2); + sliderVal = [ + isNaN(i1) ? $scope.model.config.minVal : i1 >= $scope.model.config.minVal ? i1 : $scope.model.config.minVal, + isNaN(i2) ? $scope.model.config.maxVal : i2 >= i1 ? i2 <= $scope.model.config.maxVal ? i2 : $scope.model.config.maxVal : $scope.model.config.maxVal + ]; + } else { + //this will mean it's a delimited value stored in the db, convert it to an array + sliderVal = _.map($scope.model.value.split(','), function (item) { + return parseFloat(item); + }); + } + } else { + //If no value saved yet - then use default value + if ($scope.model.value) { + sliderVal = parseFloat($scope.model.value); + } else { + sliderVal = $scope.model.config.initVal1; + } + } + // Initialise model value if not set + if (!$scope.model.value) { + setModelValueFromSlider(sliderVal); + } + //initiate slider, add event handler and get the instance reference (stored in data) + var slider = $element.find('.slider-item').bootstrapSlider({ + max: $scope.model.config.maxVal, + min: $scope.model.config.minVal, + orientation: $scope.model.config.orientation, + selection: $scope.model.config.reversed ? 'after' : 'before', + step: $scope.model.config.step, + precision: $scope.model.config.precision, + tooltip: $scope.model.config.tooltip, + tooltip_split: $scope.model.config.tooltipSplit, + tooltip_position: $scope.model.config.tooltipPosition, + handle: $scope.model.config.handle, + reversed: $scope.model.config.reversed, + ticks: $scope.model.config.ticks, + ticks_positions: $scope.model.config.ticksPositions, + ticks_labels: $scope.model.config.ticksLabels, + ticks_snap_bounds: $scope.model.config.ticksSnapBounds, + formatter: $scope.model.config.formatter, + range: $scope.model.config.enableRange, + //set the slider val - we cannot do this with data- attributes when using ranges + value: sliderVal + }).on('slideStop', function (e) { + var value = e.value; + angularHelper.safeApply($scope, function () { + setModelValueFromSlider(value); + }); + }).data('slider'); + } + /** Called on start-up when no model value has been applied and on change of the slider via the UI - updates + the model with the currently selected slider value(s) **/ + function setModelValueFromSlider(sliderVal) { + //Get the value from the slider and format it correctly, if it is a range we want a comma delimited value + if ($scope.model.config.enableRange == true) { + $scope.model.value = sliderVal.join(','); + } else { + $scope.model.value = sliderVal.toString(); + } + } + //tell the assetsService to load the bootstrap slider + //libs from the plugin folder + assetsService.loadJs('lib/slider/js/bootstrap-slider.js').then(function () { + createSlider(); + //here we declare a special method which will be called whenever the value has changed from the server + //this is instead of doing a watch on the model.value = faster + $scope.model.onValueChanged = function (newVal, oldVal) { + if (newVal != oldVal) { + createSlider(); + } + }; + }); + //load the separate css for the editor to avoid it blocking our js loading + assetsService.loadCss('lib/slider/bootstrap-slider.css'); + assetsService.loadCss('lib/slider/bootstrap-slider-custom.css'); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.SliderController', sliderController); + angular.module('umbraco').controller('Umbraco.PropertyEditors.TagsController', function ($rootScope, $scope, $log, assetsService, umbRequestHelper, angularHelper, $timeout, $element) { + var $typeahead; + $scope.isLoading = true; + $scope.tagToAdd = ''; + assetsService.loadJs('lib/typeahead.js/typeahead.bundle.min.js').then(function () { + $scope.isLoading = false; + //load current value + if ($scope.model.value) { + if (!$scope.model.config.storageType || $scope.model.config.storageType !== 'Json') { + //it is csv + if (!$scope.model.value) { + $scope.model.value = []; + } else { + if ($scope.model.value.length > 0) { + $scope.model.value = $scope.model.value.split(','); + } + } + } + } else { + $scope.model.value = []; + } + // Method required by the valPropertyValidator directive (returns true if the property editor has at least one tag selected) + $scope.validateMandatory = function () { + return { + isValid: !$scope.model.validation.mandatory || $scope.model.value != null && $scope.model.value.length > 0, + errorMsg: 'Value cannot be empty', + errorKey: 'required' + }; + }; + //Helper method to add a tag on enter or on typeahead select + function addTag(tagToAdd) { + if (tagToAdd != null && tagToAdd.length > 0) { + if ($scope.model.value.indexOf(tagToAdd) < 0) { + $scope.model.value.push(tagToAdd); + //this is required to re-validate + $scope.propertyForm.tagCount.$setViewValue($scope.model.value.length); + } + } + } + $scope.addTagOnEnter = function (e) { + var code = e.keyCode || e.which; + if (code == 13) { + //Enter keycode + if ($element.find('.tags-' + $scope.model.alias).parent().find('.tt-dropdown-menu .tt-cursor').length === 0) { + //this is required, otherwise the html form will attempt to submit. + e.preventDefault(); + $scope.addTag(); + } + } + }; + $scope.addTag = function () { + //ensure that we're not pressing the enter key whilst selecting a typeahead value from the drop down + //we need to use jquery because typeahead duplicates the text box + addTag($scope.tagToAdd); + $scope.tagToAdd = ''; + //this clears the value stored in typeahead so it doesn't try to add the text again + // http://issues.umbraco.org/issue/U4-4947 + $typeahead.typeahead('val', ''); + }; + $scope.removeTag = function (tag) { + var i = $scope.model.value.indexOf(tag); + if (i >= 0) { + $scope.model.value.splice(i, 1); + //this is required to re-validate + $scope.propertyForm.tagCount.$setViewValue($scope.model.value.length); + } + }; + //vice versa + $scope.model.onValueChanged = function (newVal, oldVal) { + //update the display val again if it has changed from the server + $scope.model.value = newVal; + if (!$scope.model.config.storageType || $scope.model.config.storageType !== 'Json') { + //it is csv + if (!$scope.model.value) { + $scope.model.value = []; + } else { + $scope.model.value = $scope.model.value.split(','); + } + } + }; + //configure the tags data source + //helper method to format the data for bloodhound + function dataTransform(list) { + //transform the result to what bloodhound wants + var tagList = _.map(list, function (i) { + return { value: i.text }; + }); + // remove current tags from the list + return $.grep(tagList, function (tag) { + return $.inArray(tag.value, $scope.model.value) === -1; + }); + } + // helper method to remove current tags + function removeCurrentTagsFromSuggestions(suggestions) { + return $.grep(suggestions, function (suggestion) { + return $.inArray(suggestion.value, $scope.model.value) === -1; + }); + } + var tagsHound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + dupDetector: function (remoteMatch, localMatch) { + return remoteMatch['value'] == localMatch['value']; + }, + //pre-fetch the tags for this category + prefetch: { + url: umbRequestHelper.getApiUrl('tagsDataBaseUrl', 'GetTags', [{ tagGroup: $scope.model.config.group }]), + //TTL = 5 minutes + ttl: 300000, + filter: dataTransform + }, + //dynamically get the tags for this category (they may have changed on the server) + remote: { + url: umbRequestHelper.getApiUrl('tagsDataBaseUrl', 'GetTags', [{ tagGroup: $scope.model.config.group }]), + filter: dataTransform + } + }); + tagsHound.initialize(true); + //configure the type ahead + $timeout(function () { + $typeahead = $element.find('.tags-' + $scope.model.alias).typeahead({ + //This causes some strangeness as it duplicates the textbox, best leave off for now. + hint: false, + highlight: true, + cacheKey: new Date(), + // Force a cache refresh each time the control is initialized + minLength: 1 + }, { + //see: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#options + // name = the data set name, we'll make this the tag group name + name: $scope.model.config.group, + displayKey: 'value', + source: function (query, cb) { + tagsHound.get(query, function (suggestions) { + cb(removeCurrentTagsFromSuggestions(suggestions)); + }); + } + }).bind('typeahead:selected', function (obj, datum, name) { + angularHelper.safeApply($scope, function () { + addTag(datum['value']); + $scope.tagToAdd = ''; + // clear the typed text + $typeahead.typeahead('val', ''); + }); + }).bind('typeahead:autocompleted', function (obj, datum, name) { + angularHelper.safeApply($scope, function () { + addTag(datum['value']); + $scope.tagToAdd = ''; + }); + }).bind('typeahead:opened', function (obj) { + }); + }); + $scope.$on('$destroy', function () { + tagsHound.clearPrefetchCache(); + tagsHound.clearRemoteCache(); + $element.find('.tags-' + $scope.model.alias).typeahead('destroy'); + delete tagsHound; + }); + }); + }); + //this controller simply tells the dialogs service to open a mediaPicker window + //with a specified callback, this callback will receive an object with a selection on it + angular.module('umbraco').controller('Umbraco.PropertyEditors.EmbeddedContentController', function ($rootScope, $scope, $log) { + $scope.showForm = false; + $scope.fakeData = []; + $scope.create = function () { + $scope.showForm = true; + $scope.fakeData = angular.copy($scope.model.config.fields); + }; + $scope.show = function () { + $scope.showCode = true; + }; + $scope.add = function () { + $scope.showForm = false; + if (!($scope.model.value instanceof Array)) { + $scope.model.value = []; + } + $scope.model.value.push(angular.copy($scope.fakeData)); + $scope.fakeData = []; + }; + }); + function textAreaController($scope) { + // macro parameter editor doesn't contains a config object, + // so we create a new one to hold any properties + if (!$scope.model.config) { + $scope.model.config = {}; + } + if (!$scope.model.config.maxChars) { + $scope.model.config.maxChars = false; + } + $scope.model.maxlength = false; + if ($scope.model.config && $scope.model.config.maxChars) { + $scope.model.maxlength = true; + if ($scope.model.value == undefined) { + $scope.model.count = $scope.model.config.maxChars * 1; + } else { + $scope.model.count = $scope.model.config.maxChars * 1 - $scope.model.value.length; + } + } + $scope.model.change = function () { + if ($scope.model.config && $scope.model.config.maxChars) { + if ($scope.model.value == undefined) { + $scope.model.count = $scope.model.config.maxChars * 1; + } else { + $scope.model.count = $scope.model.config.maxChars * 1 - $scope.model.value.length; + } + if ($scope.model.count < 0) { + $scope.model.value = $scope.model.value.substring(0, $scope.model.config.maxChars * 1); + $scope.model.count = 0; + } + } + }; + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.textAreaController', textAreaController); + function textboxController($scope) { + // macro parameter editor doesn't contains a config object, + // so we create a new one to hold any properties + if (!$scope.model.config) { + $scope.model.config = {}; + } + $scope.model.maxlength = false; + if ($scope.model.config && $scope.model.config.maxChars) { + $scope.model.maxlength = true; + } + if (!$scope.model.config.maxChars) { + // 500 is the maximum number that can be stored + // in the database, so set it to the max, even + // if no max is specified in the config + $scope.model.config.maxChars = 500; + } + if ($scope.model.maxlength) { + if ($scope.model.value === undefined) { + $scope.model.count = $scope.model.config.maxChars * 1; + } else { + $scope.model.count = $scope.model.config.maxChars * 1 - $scope.model.value.length; + } + } + $scope.model.change = function () { + if ($scope.model.config && $scope.model.config.maxChars) { + if ($scope.model.value === undefined) { + $scope.model.count = $scope.model.config.maxChars * 1; + } else { + $scope.model.count = $scope.model.config.maxChars * 1 - $scope.model.value.length; + } + if ($scope.model.count < 0) { + $scope.model.value = $scope.model.value.substring(0, $scope.model.config.maxChars * 1); + $scope.model.count = 0; + } + } + }; + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.textboxController', textboxController); + angular.module('umbraco').controller('Umbraco.PropertyEditors.UrlListController', function ($rootScope, $scope, $filter) { + function formatDisplayValue() { + if (angular.isArray($scope.model.value)) { + //it's the json value + $scope.renderModel = _.map($scope.model.value, function (item) { + return { + url: item.url, + linkText: item.linkText, + urlTarget: item.target ? item.target : '_blank', + icon: item.icon ? item.icon : 'icon-out' + }; + }); + } else { + //it's the default csv value + $scope.renderModel = _.map($scope.model.value.split(','), function (item) { + return { + url: item, + linkText: '', + urlTarget: $scope.config && $scope.config.target ? $scope.config.target : '_blank', + icon: $scope.config && $scope.config.icon ? $scope.config.icon : 'icon-out' + }; + }); + } + } + $scope.getUrl = function (valueUrl) { + if (valueUrl.indexOf('/') >= 0) { + return valueUrl; + } + return '#'; + }; + formatDisplayValue(); + //here we declare a special method which will be called whenever the value has changed from the server + //this is instead of doing a watch on the model.value = faster + $scope.model.onValueChanged = function (newVal, oldVal) { + //update the display val again + formatDisplayValue(); + }; + }); + (function () { + 'use strict'; + function ScriptsCreateController($scope, $location, navigationService, formHelper, codefileResource, localizationService, appState) { + var vm = this; + var node = $scope.dialogOptions.currentNode; + var localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); + vm.creatingFolder = false; + vm.folderName = ''; + vm.createFolderError = ''; + vm.fileExtension = ''; + vm.createFile = createFile; + vm.showCreateFolder = showCreateFolder; + vm.createFolder = createFolder; + function createFile() { + $location.path('/settings/scripts/edit/' + node.id).search('create', 'true'); + navigationService.hideMenu(); + } + function showCreateFolder() { + vm.creatingFolder = true; + } + function createFolder(form) { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: form, + statusMessage: localizeCreateFolder + })) { + codefileResource.createContainer('scripts', node.id, vm.folderName).then(function (saved) { + navigationService.hideMenu(); + navigationService.syncTree({ + tree: 'scripts', + path: saved.path, + forceReload: true, + activate: true + }); + formHelper.resetForm({ scope: $scope }); + var section = appState.getSectionState('currentSection'); + }, function (err) { + vm.createFolderError = err; + formHelper.showNotifications(err.data); + }); + } + } + } + angular.module('umbraco').controller('Umbraco.Editors.Scripts.CreateController', ScriptsCreateController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.Scripts.DeleteController + * @function + * + * @description + * The controller for deleting scripts + */ + function ScriptsDeleteController($scope, codefileResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + codefileResource.deleteByPath('scripts', $scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Scripts.DeleteController', ScriptsDeleteController); + (function () { + 'use strict'; + function ScriptsEditController($scope, $routeParams, $timeout, appState, editorState, navigationService, assetsService, codefileResource, contentEditingHelper, notificationsService, localizationService, templateHelper, angularHelper) { + var vm = this; + var currentPosition = null; + var localizeSaving = localizationService.localize('general_saving'); + vm.page = {}; + vm.page.loading = true; + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState('currentSection'); + vm.page.menu.currentNode = null; + vm.page.saveButtonState = 'init'; + //Used to toggle the keyboard shortcut modal + //From a custom keybinding in ace editor - that conflicts with our own to show the dialog + vm.showKeyboardShortcut = false; + //Keyboard shortcuts for help dialog + vm.page.keyboardShortcutsOverview = []; + vm.page.keyboardShortcutsOverview.push(templateHelper.getGeneralShortcuts()); + vm.page.keyboardShortcutsOverview.push(templateHelper.getEditorShortcuts()); + vm.script = {}; + // bind functions to view model + vm.save = save; + /* Function bound to view model */ + function save() { + vm.page.saveButtonState = 'busy'; + vm.script.content = vm.editor.getValue(); + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: codefileResource.save, + scope: $scope, + content: vm.script, + // We do not redirect on failure for scripts - this is because it is not possible to actually save the script + // when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) { + } + }).then(function (saved) { + localizationService.localizeMany([ + 'speechBubbles_fileSavedHeader', + 'speechBubbles_fileSavedText' + ]).then(function (data) { + var header = data[0]; + var message = data[1]; + notificationsService.success(header, message); + }); + //check if the name changed, if so we need to redirect + if (vm.script.id !== saved.id) { + contentEditingHelper.redirectToRenamedContent(saved.id); + } else { + vm.page.saveButtonState = 'success'; + vm.script = saved; + //sync state + editorState.set(vm.script); + // sync tree + navigationService.syncTree({ + tree: 'scripts', + path: vm.script.path, + forceReload: true + }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + }, function (err) { + vm.page.saveButtonState = 'error'; + localizationService.localizeMany([ + 'speechBubbles_validationFailedHeader', + 'speechBubbles_validationFailedMessage' + ]).then(function (data) { + var header = data[0]; + var message = data[1]; + notificationsService.error(header, message); + }); + }); + } + /* Local functions */ + function init() { + //we need to load this somewhere, for now its here. + assetsService.loadCss('lib/ace-razor-mode/theme/razor_chrome.css'); + if ($routeParams.create) { + codefileResource.getScaffold('scripts', $routeParams.id).then(function (script) { + ready(script, false); + }); + } else { + codefileResource.getByPath('scripts', $routeParams.id).then(function (script) { + ready(script, true); + }); + } + } + function ready(script, syncTree) { + vm.page.loading = false; + vm.script = script; + //sync state + editorState.set(vm.script); + if (syncTree) { + navigationService.syncTree({ + tree: 'scripts', + path: vm.script.path, + forceReload: true + }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + vm.aceOption = { + mode: 'javascript', + theme: 'chrome', + showPrintMargin: false, + advanced: { + fontSize: '14px', + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: false + }, + onLoad: function (_editor) { + vm.editor = _editor; + //Update the auto-complete method to use ctrl+alt+space + _editor.commands.bindKey('ctrl-alt-space', 'startAutocomplete'); + //Unassigns the keybinding (That was previously auto-complete) + //As conflicts with our own tree search shortcut + _editor.commands.bindKey('ctrl-space', null); + //TODO: Move all these keybinding config out into some helper/service + _editor.commands.addCommands([//Disable (alt+shift+K) + //Conflicts with our own show shortcuts dialog - this overrides it + { + name: 'unSelectOrFindPrevious', + bindKey: 'Alt-Shift-K', + exec: function () { + //Toggle the show keyboard shortcuts overlay + $scope.$apply(function () { + vm.showKeyboardShortcut = !vm.showKeyboardShortcut; + }); + }, + readOnly: true + }]); + // initial cursor placement + // Keep cursor in name field if we are create a new script + // else set the cursor at the bottom of the code editor + if (!$routeParams.create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + }); + } + vm.editor.on('change', changeAceEditor); + } + }; + function changeAceEditor() { + setFormState('dirty'); + } + function setFormState(state) { + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + // set state + if (state === 'dirty') { + currentForm.$setDirty(); + } else if (state === 'pristine') { + currentForm.$setPristine(); + } + } + } + init(); + } + angular.module('umbraco').controller('Umbraco.Editors.Scripts.EditController', ScriptsEditController); + }()); + /** + * @ngdoc controller + * @name Umbraco.Editors.Templates.DeleteController + * @function + * + * @description + * The controller for the template delete dialog + */ + function TemplatesDeleteController($scope, templateResource, treeService, navigationService) { + $scope.performDelete = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + templateResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + $scope.cancel = function () { + navigationService.hideDialog(); + }; + } + angular.module('umbraco').controller('Umbraco.Editors.Templates.DeleteController', TemplatesDeleteController); + (function () { + 'use strict'; + function TemplatesEditController($scope, $routeParams, $timeout, templateResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, treeService, contentEditingHelper, localizationService, angularHelper, templateHelper) { + var vm = this; + var oldMasterTemplateAlias = null; + var localizeSaving = localizationService.localize('general_saving'); + vm.page = {}; + vm.page.loading = true; + vm.templates = []; + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState('currentSection'); + vm.page.menu.currentNode = null; + //Used to toggle the keyboard shortcut modal + //From a custom keybinding in ace editor - that conflicts with our own to show the dialog + vm.showKeyboardShortcut = false; + //Keyboard shortcuts for help dialog + vm.page.keyboardShortcutsOverview = []; + vm.page.keyboardShortcutsOverview.push(templateHelper.getGeneralShortcuts()); + vm.page.keyboardShortcutsOverview.push(templateHelper.getEditorShortcuts()); + vm.page.keyboardShortcutsOverview.push(templateHelper.getTemplateEditorShortcuts()); + vm.save = function () { + vm.page.saveButtonState = 'busy'; + vm.template.content = vm.editor.getValue(); + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: templateResource.save, + scope: $scope, + content: vm.template, + //We do not redirect on failure for templates - this is because it is not possible to actually save the template + // type when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) { + } + }).then(function (saved) { + localizationService.localizeMany([ + 'speechBubbles_templateSavedHeader', + 'speechBubbles_templateSavedText' + ]).then(function (data) { + var header = data[0]; + var message = data[1]; + notificationsService.success(header, message); + }); + vm.page.saveButtonState = 'success'; + vm.template = saved; + //sync state + editorState.set(vm.template); + // sync tree + // if master template alias has changed move the node to it's new location + if (oldMasterTemplateAlias !== vm.template.masterTemplateAlias) { + // When creating a new template the id is -1. Make sure We don't remove the root node. + if (vm.page.menu.currentNode.id !== '-1') { + // move node to new location in tree + //first we need to remove the node that we're working on + treeService.removeNode(vm.page.menu.currentNode); + } + // update stored alias to the new one so the node won't move again unless the alias is changed again + oldMasterTemplateAlias = vm.template.masterTemplateAlias; + navigationService.syncTree({ + tree: 'templates', + path: vm.template.path, + forceReload: true, + activate: true + }).then(function (args) { + vm.page.menu.currentNode = args.node; + }); + } else { + // normal tree sync + navigationService.syncTree({ + tree: 'templates', + path: vm.template.path, + forceReload: true + }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + // clear $dirty state on form + setFormState('pristine'); + }, function (err) { + vm.page.saveButtonState = 'error'; + localizationService.localizeMany([ + 'speechBubbles_validationFailedHeader', + 'speechBubbles_validationFailedMessage' + ]).then(function (data) { + var header = data[0]; + var message = data[1]; + notificationsService.error(header, message); + }); + }); + }; + vm.init = function () { + //we need to load this somewhere, for now its here. + assetsService.loadCss('lib/ace-razor-mode/theme/razor_chrome.css'); + //load templates - used in the master template picker + templateResource.getAll().then(function (templates) { + vm.templates = templates; + }); + if ($routeParams.create) { + templateResource.getScaffold($routeParams.id).then(function (template) { + vm.ready(template); + }); + } else { + templateResource.getById($routeParams.id).then(function (template) { + vm.ready(template); + }); + } + }; + vm.ready = function (template) { + vm.page.loading = false; + vm.template = template; + //sync state + editorState.set(vm.template); + navigationService.syncTree({ + tree: 'templates', + path: vm.template.path, + forceReload: true + }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + // save state of master template to use for comparison when syncing the tree on save + oldMasterTemplateAlias = angular.copy(template.masterTemplateAlias); + // ace configuration + vm.aceOption = { + mode: 'razor', + theme: 'chrome', + showPrintMargin: false, + advanced: { + fontSize: '14px', + enableSnippets: false, + //The Razor mode snippets are awful (Need a way to override these) + enableBasicAutocompletion: true, + enableLiveAutocompletion: false + }, + onLoad: function (_editor) { + vm.editor = _editor; + //Update the auto-complete method to use ctrl+alt+space + _editor.commands.bindKey('ctrl-alt-space', 'startAutocomplete'); + //Unassigns the keybinding (That was previously auto-complete) + //As conflicts with our own tree search shortcut + _editor.commands.bindKey('ctrl-space', null); + // Assign new keybinding + _editor.commands.addCommands([ + //Disable (alt+shift+K) + //Conflicts with our own show shortcuts dialog - this overrides it + { + name: 'unSelectOrFindPrevious', + bindKey: 'Alt-Shift-K', + exec: function () { + //Toggle the show keyboard shortcuts overlay + $scope.$apply(function () { + vm.showKeyboardShortcut = !vm.showKeyboardShortcut; + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoValue', + bindKey: 'Alt-Shift-V', + exec: function () { + $scope.$apply(function () { + openPageFieldOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertPartialView', + bindKey: 'Alt-Shift-P', + exec: function () { + $scope.$apply(function () { + openPartialOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertDictionary', + bindKey: 'Alt-Shift-D', + exec: function () { + $scope.$apply(function () { + openDictionaryItemOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoMacro', + bindKey: 'Alt-Shift-M', + exec: function () { + $scope.$apply(function () { + openMacroOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertQuery', + bindKey: 'Alt-Shift-Q', + exec: function () { + $scope.$apply(function () { + openQueryBuilderOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertSection', + bindKey: 'Alt-Shift-S', + exec: function () { + $scope.$apply(function () { + openSectionsOverlay(); + }); + }, + readOnly: true + }, + { + name: 'chooseMasterTemplate', + bindKey: 'Alt-Shift-T', + exec: function () { + $scope.$apply(function () { + openMasterTemplateOverlay(); + }); + }, + readOnly: true + } + ]); + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if (!$routeParams.create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + //change on blur, focus + vm.editor.on('blur', persistCurrentLocation); + vm.editor.on('focus', persistCurrentLocation); + vm.editor.on('change', changeAceEditor); + } + }; + }; + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; + vm.openSectionsOverlay = openSectionsOverlay; + vm.openPartialOverlay = openPartialOverlay; + vm.openMasterTemplateOverlay = openMasterTemplateOverlay; + vm.selectMasterTemplate = selectMasterTemplate; + vm.getMasterTemplateName = getMasterTemplateName; + vm.removeMasterTemplate = removeMasterTemplate; + function openInsertOverlay() { + vm.insertOverlay = { + view: 'insert', + allowedTypes: { + macro: true, + dictionary: true, + partial: true, + umbracoField: true + }, + hideSubmitButton: true, + show: true, + submit: function (model) { + switch (model.insert.type) { + case 'macro': + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, 'Mvc'); + insert(macroObject.syntax); + break; + case 'dictionary': + var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); + insert(code); + break; + case 'partial': + var code = templateHelper.getInsertPartialSnippet(model.insert.node.parentId, model.insert.node.name); + insert(code); + break; + case 'umbracoField': + insert(model.insert.umbracoField); + break; + } + vm.insertOverlay.show = false; + vm.insertOverlay = null; + }, + close: function (oldModel) { + // close the dialog + vm.insertOverlay.show = false; + vm.insertOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openMacroOverlay() { + vm.macroPickerOverlay = { + view: 'macropicker', + dialogData: {}, + show: true, + title: localizationService.localize('template_insertMacro'), + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, 'Mvc'); + insert(macroObject.syntax); + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + }, + close: function (oldModel) { + // close the dialog + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + // focus editor + vm.editor.focus(); + } + }; } - }; - - $scope.initSection = function (section) { - section.$percentage = $scope.percentage(section.grid); - - section.$allowedLayouts = getAllowedLayouts(section); - - if (!section.rows || section.rows.length === 0) { - section.rows = []; - if(section.$allowedLayouts.length === 1){ - $scope.addRow(section, section.$allowedLayouts[0]); - } - } else { - _.forEach(section.rows, function (row, index) { - if (!row.$initialized) { - var initd = $scope.initRow(row); - - //if init fails, remove - if (!initd) { - section.rows.splice(index, 1); - } else { - section.rows[index] = initd; + function openPageFieldOverlay() { + vm.pageFieldOverlay = { + submitButtonLabel: 'Insert', + closeButtonlabel: 'Cancel', + view: 'insertfield', + show: true, + title: localizationService.localize('template_insertPageField'), + submit: function (model) { + insert(model.umbracoField); + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + }, + close: function (model) { + // close the dialog + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openDictionaryItemOverlay() { + vm.dictionaryItemOverlay = { + view: 'treepicker', + section: 'settings', + treeAlias: 'dictionary', + entityType: 'dictionary', + multiPicker: false, + show: true, + title: localizationService.localize('template_insertDictionaryItem'), + emptyStateMessage: localizationService.localize('emptyStates_emptyDictionaryTree'), + select: function (node) { + var code = templateHelper.getInsertDictionarySnippet(node.name); + insert(code); + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + }, + close: function (model) { + // close dialog + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openPartialOverlay() { + vm.partialItemOverlay = { + view: 'treepicker', + section: 'settings', + treeAlias: 'partialViews', + entityType: 'partialView', + multiPicker: false, + show: true, + title: localizationService.localize('template_insertPartialView'), + filter: function (i) { + if (i.name.indexOf('.cshtml') === -1 && i.name.indexOf('.vbhtml') === -1) { + return true; } + }, + filterCssClass: 'not-allowed', + select: function (node) { + var code = templateHelper.getInsertPartialSnippet(node.parentId, node.name); + insert(code); + vm.partialItemOverlay.show = false; + vm.partialItemOverlay = null; + }, + close: function (model) { + // close dialog + vm.partialItemOverlay.show = false; + vm.partialItemOverlay = null; + // focus editor + vm.editor.focus(); } - }); - - // if there is more than one row added - hide row add tools - $scope.showRowConfigurations = false; + }; } - }; - - - // ********************************************* - // Init layout / row - // ********************************************* - $scope.initRow = function (row) { - - //merge the layout data with the original config data - //if there are no config info on this, splice it out - var original = _.find($scope.model.config.items.layouts, function (o) { return o.name === row.name; }); - - if (!original) { - return null; - } else { - //make a copy to not touch the original config - original = angular.copy(original); - original.styles = row.styles; - original.config = row.config; - original.hasConfig = gridItemHasConfig(row.styles, row.config); - - - //sync area configuration - _.each(original.areas, function (area, areaIndex) { - - - if (area.grid > 0) { - var currentArea = row.areas[areaIndex]; - - if (currentArea) { - area.config = currentArea.config; - area.styles = currentArea.styles; - area.hasConfig = gridItemHasConfig(currentArea.styles, currentArea.config); + function openQueryBuilderOverlay() { + vm.queryBuilderOverlay = { + view: 'querybuilder', + show: true, + title: localizationService.localize('template_queryBuilder'), + submit: function (model) { + var code = templateHelper.getQuerySnippet(model.result.queryExpression); + insert(code); + vm.queryBuilderOverlay.show = false; + vm.queryBuilderOverlay = null; + }, + close: function (model) { + // close dialog + vm.queryBuilderOverlay.show = false; + vm.queryBuilderOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openSectionsOverlay() { + vm.sectionsOverlay = { + view: 'templatesections', + isMaster: vm.template.isMasterTemplate, + submitButtonLabel: 'Insert', + show: true, + submit: function (model) { + if (model.insertType === 'renderBody') { + var code = templateHelper.getRenderBodySnippet(); + insert(code); } - - //set editor permissions - if (!area.allowed || area.allowAll === true) { - area.$allowedEditors = $scope.availableEditors; - area.$allowsRTE = true; - } else { - area.$allowedEditors = _.filter($scope.availableEditors, function (editor) { - return _.indexOf(area.allowed, editor.alias) >= 0; - }); - - if (_.indexOf(area.allowed, "rte") >= 0) { - area.$allowsRTE = true; - } + if (model.insertType === 'renderSection') { + var code = templateHelper.getRenderSectionSnippet(model.renderSectionName, model.mandatoryRenderSection); + insert(code); } - - //copy over existing controls into the new areas - if (row.areas.length > areaIndex && row.areas[areaIndex].controls) { - area.controls = currentArea.controls; - - _.forEach(area.controls, function (control, controlIndex) { - $scope.initControl(control, controlIndex); - }); - - } else { - //if empty - area.controls = []; - - //if only one allowed editor - if(area.$allowedEditors.length === 1){ - $scope.addControl(area.$allowedEditors[0], area, 0, false); - } + if (model.insertType === 'addSection') { + var code = templateHelper.getAddSectionSnippet(model.sectionName); + wrap(code); } - - //set width - area.$percentage = $scope.percentage(area.grid); - area.$uniqueId = $scope.setUniqueId(); - - } else { - original.areas.splice(areaIndex, 1); + vm.sectionsOverlay.show = false; + vm.sectionsOverlay = null; + }, + close: function (model) { + // close dialog + vm.sectionsOverlay.show = false; + vm.sectionsOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + function openMasterTemplateOverlay() { + // make collection of available master templates + var availableMasterTemplates = []; + // filter out the current template and the selected master template + angular.forEach(vm.templates, function (template) { + if (template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { + availableMasterTemplates.push(template); } }); - - //replace the old row - original.$initialized = true; - - //set a disposable unique ID - original.$uniqueId = $scope.setUniqueId(); - - //set a no disposable unique ID (util for row styling) - original.id = !row.id ? $scope.setUniqueId() : row.id; - - return original; + vm.masterTemplateOverlay = { + view: 'itempicker', + title: localizationService.localize('template_mastertemplate'), + availableItems: availableMasterTemplates, + show: true, + submit: function (model) { + var template = model.selectedItem; + if (template && template.alias) { + vm.template.masterTemplateAlias = template.alias; + setLayout(template.alias + '.cshtml'); + } else { + vm.template.masterTemplateAlias = null; + setLayout(null); + } + vm.masterTemplateOverlay.show = false; + vm.masterTemplateOverlay = null; + }, + close: function (oldModel) { + // close dialog + vm.masterTemplateOverlay.show = false; + vm.masterTemplateOverlay = null; + // focus editor + vm.editor.focus(); + } + }; } - - }; - - - // ********************************************* - // Init control - // ********************************************* - - $scope.initControl = function (control, index) { - control.$index = index; - control.$uniqueId = $scope.setUniqueId(); - - //error handling in case of missing editor.. - //should only happen if stripped earlier - if (!control.editor) { - control.$editorPath = "views/propertyeditors/grid/editors/error.html"; + function selectMasterTemplate(template) { + if (template && template.alias) { + vm.template.masterTemplateAlias = template.alias; + setLayout(template.alias + '.cshtml'); + } else { + vm.template.masterTemplateAlias = null; + setLayout(null); + } } - - if (!control.$editorPath) { - var editorConfig = $scope.getEditor(control.editor.alias); - - if (editorConfig) { - control.editor = editorConfig; - - //if its an absolute path - if (control.editor.view.startsWith("/") || control.editor.view.startsWith("~/")) { - control.$editorPath = umbRequestHelper.convertVirtualToAbsolutePath(control.editor.view); + function getMasterTemplateName(masterTemplateAlias, templates) { + if (masterTemplateAlias) { + var templateName = ''; + angular.forEach(templates, function (template) { + if (template.alias === masterTemplateAlias) { + templateName = template.name; + } + }); + return templateName; + } + } + function removeMasterTemplate() { + vm.template.masterTemplateAlias = null; + // call set layout with no paramters to set layout to null + setLayout(); + } + function setLayout(templatePath) { + var templateCode = vm.editor.getValue(); + var newValue = templatePath; + var layoutDefRegex = new RegExp('(@{[\\s\\S]*?Layout\\s*?=\\s*?)("[^"]*?"|null)(;[\\s\\S]*?})', 'gi'); + if (newValue !== undefined && newValue !== '') { + if (layoutDefRegex.test(templateCode)) { + // Declaration exists, so just update it + templateCode = templateCode.replace(layoutDefRegex, '$1"' + newValue + '"$3'); + } else { + // Declaration doesn't exist, so prepend to start of doc + //TODO: Maybe insert at the cursor position, rather than just at the top of the doc? + templateCode = '@{\n\tLayout = "' + newValue + '";\n}\n' + templateCode; } - else { - //use convention - control.$editorPath = "views/propertyeditors/grid/editors/" + control.editor.view + ".html"; + } else { + if (layoutDefRegex.test(templateCode)) { + // Declaration exists, so just update it + templateCode = templateCode.replace(layoutDefRegex, '$1null$3'); } } - else { - control.$editorPath = "views/propertyeditors/grid/editors/error.html"; + vm.editor.setValue(templateCode); + vm.editor.clearSelection(); + vm.editor.navigateFileStart(); + vm.editor.focus(); + // set form state to $dirty + setFormState('dirty'); + } + function insert(str) { + vm.editor.focus(); + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + // set form state to $dirty + setFormState('dirty'); + } + function wrap(str) { + var selectedContent = vm.editor.session.getTextRange(vm.editor.getSelectionRange()); + str = str.replace('{0}', selectedContent); + vm.editor.insert(str); + vm.editor.focus(); + // set form state to $dirty + setFormState('dirty'); + } + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + function changeAceEditor() { + setFormState('dirty'); + } + function setFormState(state) { + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + // set state + if (state === 'dirty') { + currentForm.$setDirty(); + } else if (state === 'pristine') { + currentForm.$setPristine(); } } - - - }; - - - gridService.getGridEditors().then(function (response) { - $scope.availableEditors = response.data; - - $scope.contentReady = true; - - // ********************************************* - // Init grid - // ********************************************* - $scope.initContent(); - - }); - - //Clean the grid value before submitting to the server, we don't need - // all of that grid configuration in the value to be stored!! All of that - // needs to be merged in at runtime to ensure that the real config values are used - // if they are ever updated. - - var unsubscribe = $scope.$on("formSubmitting", function () { - - if ($scope.model.value && $scope.model.value.sections) { - _.each($scope.model.value.sections, function(section) { - if (section.rows) { - _.each(section.rows, function (row) { - if (row.areas) { - _.each(row.areas, function (area) { - - //Remove the 'editors' - these are the allowed editors, these will - // be injected at runtime to this editor, it should not be persisted - - if (area.editors) { - delete area.editors; - } - - if (area.controls) { - _.each(area.controls, function (control) { - if (control.editor) { - //replace - var alias = control.editor.alias; - control.editor = { - alias: alias - }; - } - }); - } - }); + vm.init(); + } + angular.module('umbraco').controller('Umbraco.Editors.Templates.EditController', TemplatesEditController); + }()); + (function () { + 'use strict'; + function UserGroupEditController($scope, $location, $routeParams, userGroupsResource, localizationService, contentEditingHelper) { + var vm = this; + var localizeSaving = localizationService.localize('general_saving'); + vm.page = {}; + vm.page.rootIcon = 'icon-folder'; + vm.userGroup = {}; + vm.labels = {}; + vm.goToPage = goToPage; + vm.openSectionPicker = openSectionPicker; + vm.openContentPicker = openContentPicker; + vm.openMediaPicker = openMediaPicker; + vm.openUserPicker = openUserPicker; + vm.removeSelectedItem = removeSelectedItem; + vm.clearStartNode = clearStartNode; + vm.save = save; + vm.openGranularPermissionsPicker = openGranularPermissionsPicker; + vm.setPermissionsForNode = setPermissionsForNode; + function init() { + vm.loading = true; + var labelKeys = [ + 'general_cancel', + 'defaultdialogs_selectContentStartNode', + 'defaultdialogs_selectMediaStartNode', + 'defaultdialogs_selectNode', + 'general_groups', + 'content_contentRoot', + 'media_mediaRoot' + ]; + localizationService.localizeMany(labelKeys).then(function (values) { + vm.labels.cancel = values[0]; + vm.labels.selectContentStartNode = values[1]; + vm.labels.selectMediaStartNode = values[2]; + vm.labels.selectNode = values[3]; + vm.labels.groups = values[4]; + vm.labels.contentRoot = values[5]; + vm.labels.mediaRoot = values[6]; + }); + localizationService.localize('general_add').then(function (name) { + vm.labels.add = name; + }); + localizationService.localize('user_noStartNode').then(function (name) { + vm.labels.noStartNode = name; + }); + if ($routeParams.create) { + // get user group scaffold + userGroupsResource.getUserGroupScaffold().then(function (userGroup) { + vm.userGroup = userGroup; + setSectionIcon(vm.userGroup.sections); + makeBreadcrumbs(); + vm.loading = false; + }); + } else { + // get user group + userGroupsResource.getUserGroup($routeParams.id).then(function (userGroup) { + vm.userGroup = userGroup; + formatGranularPermissionSelection(); + setSectionIcon(vm.userGroup.sections); + makeBreadcrumbs(); + vm.loading = false; + }); + } + } + function save() { + vm.page.saveButtonState = 'busy'; + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: userGroupsResource.saveUserGroup, + scope: $scope, + content: vm.userGroup, + // We do not redirect on failure for users - this is because it is not possible to actually save a user + // when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) { + } + }).then(function (saved) { + vm.userGroup = saved; + formatGranularPermissionSelection(); + setSectionIcon(vm.userGroup.sections); + makeBreadcrumbs(); + vm.page.saveButtonState = 'success'; + }, function (err) { + vm.page.saveButtonState = 'error'; + }); + } + function goToPage(ancestor) { + $location.path(ancestor.path).search('subview', ancestor.subView); + } + function openSectionPicker() { + vm.sectionPicker = { + view: 'sectionpicker', + selection: vm.userGroup.sections, + closeButtonLabel: vm.labels.cancel, + show: true, + submit: function (model) { + vm.sectionPicker.show = false; + vm.sectionPicker = null; + }, + close: function (oldModel) { + if (oldModel.selection) { + vm.userGroup.sections = oldModel.selection; + } + vm.sectionPicker.show = false; + vm.sectionPicker = null; + } + }; + } + function openContentPicker() { + vm.contentPicker = { + title: vm.labels.selectContentStartNode, + view: 'contentpicker', + hideSubmitButton: true, + hideHeader: false, + show: true, + submit: function (model) { + if (model.selection) { + vm.userGroup.contentStartNode = model.selection[0]; + if (vm.userGroup.contentStartNode.id === '-1') { + vm.userGroup.contentStartNode.name = vm.labels.contentRoot; + vm.userGroup.contentStartNode.icon = 'icon-folder'; } - }); + } + vm.contentPicker.show = false; + vm.contentPicker = null; + }, + close: function (oldModel) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } + }; + } + function openMediaPicker() { + vm.contentPicker = { + title: vm.labels.selectMediaStartNode, + view: 'treepicker', + section: 'media', + treeAlias: 'media', + entityType: 'media', + hideSubmitButton: true, + hideHeader: false, + show: true, + submit: function (model) { + if (model.selection) { + vm.userGroup.mediaStartNode = model.selection[0]; + if (vm.userGroup.mediaStartNode.id === '-1') { + vm.userGroup.mediaStartNode.name = vm.labels.mediaRoot; + vm.userGroup.mediaStartNode.icon = 'icon-folder'; + } + } + vm.contentPicker.show = false; + vm.contentPicker = null; + }, + close: function (oldModel) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } + }; + } + function openUserPicker() { + vm.userPicker = { + view: 'userpicker', + selection: vm.userGroup.users, + show: true, + submit: function (model) { + vm.userPicker.show = false; + vm.userPicker = null; + }, + close: function (oldModel) { + vm.userPicker.show = false; + vm.userPicker = null; } + }; + } + /** + * The granular permissions structure gets returned from the server in the dictionary format with each key being the permission category + * however the list to display the permissions isn't via the dictionary way so we need to format it + */ + function formatGranularPermissionSelection() { + angular.forEach(vm.userGroup.assignedPermissions, function (node) { + formatGranularPermissionSelectionForNode(node); }); } - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on("$destroy", function () { - unsubscribe(); - }); - - }); - -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.GridPrevalueEditorController", - function ($scope, $http, assetsService, $rootScope, dialogService, mediaResource, gridService, imageHelper, $timeout) { - - var emptyModel = { - styles:[ - { - label: "Set a background image", - description: "Set a row background", - key: "background-image", - view: "imagepicker", - modifier: "url({0})" - } - ], - - config:[ - { - label: "Class", - description: "Set a css class", - key: "class", - view: "textstring" - } - ], - - columns: 12, - templates:[ - { - name: "1 column layout", - sections: [ - { - grid: 12, + function formatGranularPermissionSelectionForNode(node) { + //the dictionary is assigned via node.permissions we will reformat to node.allowedPermissions + node.allowedPermissions = []; + angular.forEach(node.permissions, function (permissions, key) { + angular.forEach(permissions, function (p) { + if (p.checked) { + node.allowedPermissions.push(p); } - ] - }, - { - name: "2 column layout", - sections: [ - { - grid: 4, - }, - { - grid: 8 + }); + }); + } + function openGranularPermissionsPicker() { + vm.contentPicker = { + title: vm.labels.selectNode, + view: 'contentpicker', + hideSubmitButton: true, + show: true, + submit: function (model) { + if (model.selection) { + var node = model.selection[0]; + //check if this is already in our selection + var found = _.find(vm.userGroup.assignedPermissions, function (i) { + return i.id === node.id; + }); + node = found ? found : node; + setPermissionsForNode(node); } - ] + }, + close: function (oldModel) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } + }; + } + function setPermissionsForNode(node) { + //clone the current defaults to pass to the model + if (!node.permissions) { + node.permissions = angular.copy(vm.userGroup.defaultPermissions); } - ], - - - layouts:[ - { - label: "Headline", - name: "Headline", - areas: [ - { - grid: 12, - editors: ["headline"] + vm.nodePermissions = { + view: 'nodepermissions', + node: node, + show: true, + submit: function (model) { + if (model && model.node && model.node.permissions) { + formatGranularPermissionSelectionForNode(node); + if (!vm.userGroup.assignedPermissions) { + vm.userGroup.assignedPermissions = []; + } + //check if this is already in our selection + var found = _.find(vm.userGroup.assignedPermissions, function (i) { + return i.id === node.id; + }); + if (!found) { + vm.userGroup.assignedPermissions.push(node); + } } - ] - }, - { - label: "Article", - name: "Article", - areas: [ - { - grid: 4 - }, - { - grid: 8 + // close node permisssions overlay + vm.nodePermissions.show = false; + vm.nodePermissions = null; + // close content picker overlay + if (vm.contentPicker) { + vm.contentPicker.show = false; + vm.contentPicker = null; } - ] - } - ] - }; - - /**************** - template - *****************/ - - $scope.configureTemplate = function(template) { - - var templatesCopy = angular.copy($scope.model.value.templates); - - if (template === undefined) { - template = { - name: "", - sections: [ - - ] - }; - $scope.model.value.templates.push(template); - } - - $scope.layoutConfigOverlay = {}; - $scope.layoutConfigOverlay.view = "views/propertyEditors/grid/dialogs/layoutconfig.html"; - $scope.layoutConfigOverlay.currentLayout = template; - $scope.layoutConfigOverlay.rows = $scope.model.value.layouts; - $scope.layoutConfigOverlay.columns = $scope.model.value.columns; - $scope.layoutConfigOverlay.show = true; - - $scope.layoutConfigOverlay.submit = function(model) { - $scope.layoutConfigOverlay.show = false; - $scope.layoutConfigOverlay = null; - }; - - $scope.layoutConfigOverlay.close = function(oldModel) { - - //reset templates - $scope.model.value.templates = templatesCopy; - - $scope.layoutConfigOverlay.show = false; - $scope.layoutConfigOverlay = null; - } - - }; - - $scope.deleteTemplate = function(index){ - $scope.model.value.templates.splice(index, 1); - }; - - - /**************** - Row - *****************/ - - $scope.configureLayout = function(layout) { - - var layoutsCopy = angular.copy($scope.model.value.layouts); - - if(layout === undefined){ - layout = { - name: "", - areas:[ - - ] + }, + close: function (oldModel) { + vm.nodePermissions.show = false; + vm.nodePermissions = null; + } }; - $scope.model.value.layouts.push(layout); - } - - $scope.rowConfigOverlay = {}; - $scope.rowConfigOverlay.view = "views/propertyEditors/grid/dialogs/rowconfig.html"; - $scope.rowConfigOverlay.currentRow = layout; - $scope.rowConfigOverlay.editors = $scope.editors; - $scope.rowConfigOverlay.columns = $scope.model.value.columns; - $scope.rowConfigOverlay.show = true; - - $scope.rowConfigOverlay.submit = function(model) { - $scope.rowConfigOverlay.show = false; - $scope.rowConfigOverlay = null; - }; - - $scope.rowConfigOverlay.close = function(oldModel) { - $scope.model.value.layouts = layoutsCopy; - $scope.rowConfigOverlay.show = false; - $scope.rowConfigOverlay = null; - }; - - }; - - //var rowDeletesPending = false; - $scope.deleteLayout = function(index) { - - $scope.rowDeleteOverlay = {}; - $scope.rowDeleteOverlay.view = "views/propertyEditors/grid/dialogs/rowdeleteconfirm.html"; - $scope.rowDeleteOverlay.dialogData = { - rowName: $scope.model.value.layouts[index].name - }; - $scope.rowDeleteOverlay.show = true; - - $scope.rowDeleteOverlay.submit = function(model) { - - $scope.model.value.layouts.splice(index, 1); - - $scope.rowDeleteOverlay.show = false; - $scope.rowDeleteOverlay = null; - }; - - $scope.rowDeleteOverlay.close = function(oldModel) { - $scope.rowDeleteOverlay.show = false; - $scope.rowDeleteOverlay = null; - }; - - }; - - - /**************** - utillities - *****************/ - $scope.toggleCollection = function(collection, toggle){ - if(toggle){ - collection = []; - }else{ - delete collection; } - }; - - $scope.percentage = function(spans){ - return ((spans / $scope.model.value.columns) * 100).toFixed(8); - }; - - $scope.zeroWidthFilter = function (cell) { - return cell.grid > 0; - }; - - /**************** - Config - *****************/ - - $scope.removeConfigValue = function(collection, index){ - collection.splice(index, 1); - }; - - var editConfigCollection = function(configValues, title, callback) { - - $scope.editConfigCollectionOverlay = {}; - $scope.editConfigCollectionOverlay.view = "views/propertyeditors/grid/dialogs/editconfig.html"; - $scope.editConfigCollectionOverlay.config = configValues; - $scope.editConfigCollectionOverlay.title = title; - $scope.editConfigCollectionOverlay.show = true; - - $scope.editConfigCollectionOverlay.submit = function(model) { - - callback(model.config) - - $scope.editConfigCollectionOverlay.show = false; - $scope.editConfigCollectionOverlay = null; - }; - - $scope.editConfigCollectionOverlay.close = function(oldModel) { - $scope.editConfigCollectionOverlay.show = false; - $scope.editConfigCollectionOverlay = null; - }; - - }; - - $scope.editConfig = function() { - editConfigCollection($scope.model.value.config, "Settings", function(data) { - $scope.model.value.config = data; - }); - }; - - $scope.editStyles = function() { - editConfigCollection($scope.model.value.styles, "Styling", function(data){ - $scope.model.value.styles = data; - }); - }; - - /**************** - editors - *****************/ - gridService.getGridEditors().then(function(response){ - $scope.editors = response.data; - }); - - - /* init grid data */ - if (!$scope.model.value || $scope.model.value === "" || !$scope.model.value.templates) { - $scope.model.value = emptyModel; - } else { - - if (!$scope.model.value.columns) { - $scope.model.value.columns = emptyModel.columns; + function removeSelectedItem(index, selection) { + if (selection && selection.length > 0) { + selection.splice(index, 1); + } } - - - if (!$scope.model.value.config) { - $scope.model.value.config = []; + function clearStartNode(type) { + if (type === 'content') { + vm.userGroup.contentStartNode = null; + } else if (type === 'media') { + vm.userGroup.mediaStartNode = null; + } } - - if (!$scope.model.value.styles) { - $scope.model.value.styles = []; + function makeBreadcrumbs() { + vm.breadcrumbs = [ + { + 'name': vm.labels.groups, + 'path': '/users/users/overview', + 'subView': 'groups' + }, + { 'name': vm.userGroup.name } + ]; } - } - - /**************** - Clean up - *****************/ - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var ts = $scope.model.value.templates; - var ls = $scope.model.value.layouts; - - _.each(ts, function(t){ - _.each(t.sections, function(section, index){ - if(section.grid === 0){ - t.sections.splice(index, 1); - } - }); - }); - - _.each(ls, function(l){ - _.each(l.areas, function(area, index){ - if(area.grid === 0){ - l.areas.splice(index, 1); - } - }); - }); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - }); - -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -angular.module('umbraco') - .controller("Umbraco.PropertyEditors.ImageCropperController", - function ($rootScope, $routeParams, $scope, $log, mediaHelper, cropperHelper, $timeout, editorState, umbRequestHelper, fileManager, angularHelper) { - - var config = angular.copy($scope.model.config); - $scope.imageIsLoaded = false; - - //move previously saved value to the editor - if ($scope.model.value) { - //backwards compat with the old file upload (incase some-one swaps them..) - if (angular.isString($scope.model.value)) { - config.src = $scope.model.value; - $scope.model.value = config; - } else if ($scope.model.value.crops) { - //sync any config changes with the editor and drop outdated crops - _.each($scope.model.value.crops, function (saved) { - var configured = _.find(config.crops, function (item) { return item.alias === saved.alias }); - - if (configured && configured.height === saved.height && configured.width === saved.width) { - configured.coordinates = saved.coordinates; - } + function setSectionIcon(sections) { + angular.forEach(sections, function (section) { + section.icon = 'icon-section ' + section.cssclass; }); - $scope.model.value.crops = config.crops; - - //restore focalpoint if missing - if (!$scope.model.value.focalPoint) { - $scope.model.value.focalPoint = { left: 0.5, top: 0.5 }; - } } - - $scope.imageSrc = $scope.model.value.src; + init(); } - - - //crop a specific crop - $scope.crop = function (crop) { - $scope.currentCrop = crop; - $scope.currentPoint = undefined; - }; - - //done cropping - $scope.done = function () { - $scope.currentCrop = undefined; - $scope.currentPoint = undefined; - }; - - //crop a specific crop - $scope.clear = function (crop) { - //clear current uploaded files - fileManager.setFiles($scope.model.alias, []); - - //clear the ui - $scope.imageSrc = undefined; - if ($scope.model.value) { - delete $scope.model.value; + angular.module('umbraco').controller('Umbraco.Editors.Users.GroupController', UserGroupEditController); + }()); + (function () { + 'use strict'; + function UsersOverviewController($scope, $location, $timeout, navigationService, localizationService) { + var vm = this; + var usersUri = $location.search().subview; + if (!usersUri) { + $location.search('subview', 'users'); + //exit after this, we don't want to initialize anything further since this + //is going to change the route + return; } - - // set form to dirty to tricker discard changes dialog - var currForm = angularHelper.getCurrentForm($scope); - currForm.$setDirty(); - }; - - //show previews - $scope.togglePreviews = function () { - if ($scope.showPreviews) { - $scope.showPreviews = false; - $scope.tempShowPreviews = false; - } else { - $scope.showPreviews = true; + //note on the below, we dont assign a view unless it's the right route since if we did that it will load in that controller + //for the view which is unecessary and will cause extra overhead/requests to occur + vm.page = {}; + vm.page.name = localizationService.localize('user_userManagement'); + vm.page.navigation = [ + { + 'name': localizationService.localize('sections_users'), + 'icon': 'icon-user', + 'action': function () { + $location.search('subview', 'users'); + }, + 'view': !usersUri || usersUri === 'users' ? 'views/users/views/users/users.html' : null, + 'active': !usersUri || usersUri === 'users' + }, + { + 'name': localizationService.localize('general_groups'), + 'icon': 'icon-users', + 'action': function () { + $location.search('subview', 'groups'); + }, + 'view': usersUri === 'groups' ? 'views/users/views/groups/groups.html' : null, + 'active': usersUri === 'groups' + } + ]; + function init() { + $timeout(function () { + navigationService.syncTree({ + tree: 'users', + path: '-1' + }); + }); } - }; - - $scope.imageLoaded = function() { - $scope.imageIsLoaded = true; - }; - - //on image selected, update the cropper - $scope.$on("filesSelected", function (ev, args) { - $scope.model.value = config; - - if (args.files && args.files[0]) { - - fileManager.setFiles($scope.model.alias, args.files); - - var reader = new FileReader(); - reader.onload = function (e) { - - $scope.$apply(function () { - $scope.imageSrc = e.target.result; + init(); + } + angular.module('umbraco').controller('Umbraco.Editors.Users.OverviewController', UsersOverviewController); + }()); + (function () { + 'use strict'; + function UserEditController($scope, $timeout, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) { + var vm = this; + vm.page = {}; + vm.page.rootIcon = 'icon-folder'; + vm.user = { changePassword: null }; + vm.breadcrumbs = []; + vm.avatarFile = {}; + vm.labels = {}; + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + 'KB'; + vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); + vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail; + //create the initial model for change password + vm.changePasswordModel = { + config: {}, + isChanging: false + }; + vm.goToPage = goToPage; + vm.openUserGroupPicker = openUserGroupPicker; + vm.openContentPicker = openContentPicker; + vm.openMediaPicker = openMediaPicker; + vm.removeSelectedItem = removeSelectedItem; + vm.disableUser = disableUser; + vm.enableUser = enableUser; + vm.unlockUser = unlockUser; + vm.clearAvatar = clearAvatar; + vm.save = save; + vm.toggleChangePassword = toggleChangePassword; + function init() { + vm.loading = true; + var labelKeys = [ + 'general_saving', + 'general_cancel', + 'defaultdialogs_selectContentStartNode', + 'defaultdialogs_selectMediaStartNode', + 'sections_users', + 'content_contentRoot', + 'media_mediaRoot', + 'user_noStartNodes' + ]; + localizationService.localizeMany(labelKeys).then(function (values) { + vm.labels.saving = values[0]; + vm.labels.cancel = values[1]; + vm.labels.selectContentStartNode = values[2]; + vm.labels.selectMediaStartNode = values[3]; + vm.labels.users = values[4]; + vm.labels.contentRoot = values[5]; + vm.labels.mediaRoot = values[6]; + vm.labels.noStartNodes = values[7]; + }); + // get user + usersResource.getUser($routeParams.id).then(function (user) { + vm.user = user; + makeBreadcrumbs(vm.user); + setUserDisplayState(); + formatDatesToLocal(vm.user); + vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail && user.email === user.username; + //go get the config for the membership provider and add it to the model + authResource.getMembershipProviderConfig().then(function (data) { + vm.changePasswordModel.config = data; + //the user has a password if they are not states: Invited, NoCredentials + vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; + vm.changePasswordModel.config.disableToggle = true; + //this is only relavent for membership providers now (it's basically obsolete) + vm.changePasswordModel.config.enableReset = false; + //in the ASP.NET Identity world, this config option will allow an admin user to change another user's password + //if the user has access to the user section. So if this editor is being access, the user of course has access to this section. + //the authorization check is also done on the server side when submitted. + vm.changePasswordModel.config.allowManuallyChangingPassword = !vm.user.isCurrentUser; + vm.loading = false; }); - - }; - - reader.readAsDataURL(args.files[0]); + }); } - }); - - - //here we declare a special method which will be called whenever the value has changed from the server - $scope.model.onValueChanged = function (newVal, oldVal) { - //clear current uploaded files - fileManager.setFiles($scope.model.alias, []); - }; - - var unsubscribe = $scope.$on("formSubmitting", function () { - $scope.done(); - }); - - $scope.$on('$destroy', function () { - unsubscribe(); - }); - }) - .run(function (mediaHelper, umbRequestHelper) { - if (mediaHelper && mediaHelper.registerFileResolver) { - - //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource - // they contain different data structures so if we need to query against it we need to be aware of this. - mediaHelper.registerFileResolver("Umbraco.ImageCropper", function (property, entity, thumbnail) { - if (property.value && property.value.src) { - - if (thumbnail === true) { - return property.value.src + "?width=500&mode=max&animationprocessmode=first"; - } - else { - return property.value.src; + function getLocalDate(date, culture, format) { + if (date) { + var dateVal; + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + var localOffset = new Date().getTimezoneOffset(); + var serverTimeNeedsOffsetting = -serverOffset !== localOffset; + if (serverTimeNeedsOffsetting) { + dateVal = dateHelper.convertToLocalMomentTime(date, serverOffset); + } else { + dateVal = moment(date, 'YYYY-MM-DD HH:mm:ss'); } - - //this is a fallback in case the cropper has been asssigned a upload field + return dateVal.locale(culture).format(format); } - else if (angular.isString(property.value)) { - if (thumbnail) { - - if (mediaHelper.detectIfImageByExtension(property.value)) { - - var thumbnailUrl = umbRequestHelper.getApiUrl( - "imagesApiBaseUrl", - "GetBigThumbnail", - [{ originalImagePath: property.value }]); - - return thumbnailUrl; + } + function toggleChangePassword() { + vm.changePasswordModel.isChanging = !vm.changePasswordModel.isChanging; + //reset it + vm.user.changePassword = null; + } + function save() { + vm.page.saveButtonState = 'busy'; + vm.user.resetPasswordValue = null; + //anytime a user is changing another user's password, we are in effect resetting it so we need to set that flag here + if (vm.user.changePassword) { + vm.user.changePassword.reset = !vm.user.changePassword.oldPassword && !vm.user.isCurrentUser; + } + contentEditingHelper.contentEditorPerformSave({ + statusMessage: vm.labels.saving, + saveMethod: usersResource.saveUser, + scope: $scope, + content: vm.user, + // We do not redirect on failure for users - this is because it is not possible to actually save a user + // when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) { + } + }).then(function (saved) { + vm.user = saved; + setUserDisplayState(); + formatDatesToLocal(vm.user); + vm.changePasswordModel.isChanging = false; + vm.page.saveButtonState = 'success'; + //the user has a password if they are not states: Invited, NoCredentials + vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; + }, function (err) { + vm.page.saveButtonState = 'error'; + }); + } + function goToPage(ancestor) { + $location.path(ancestor.path).search('subview', ancestor.subView); + } + function openUserGroupPicker() { + vm.userGroupPicker = { + view: 'usergrouppicker', + selection: vm.user.userGroups, + closeButtonLabel: vm.labels.cancel, + show: true, + submit: function (model) { + // apply changes + if (model.selection) { + vm.user.userGroups = model.selection; } - else { - return null; + vm.userGroupPicker.show = false; + vm.userGroupPicker = null; + }, + close: function (oldModel) { + // rollback on close + if (oldModel.selection) { + vm.user.userGroups = oldModel.selection; + } + vm.userGroupPicker.show = false; + vm.userGroupPicker = null; + } + }; + } + function openContentPicker() { + vm.contentPicker = { + title: vm.labels.selectContentStartNode, + view: 'contentpicker', + multiPicker: true, + selection: vm.user.startContentIds, + hideHeader: false, + show: true, + submit: function (model) { + // select items + if (model.selection) { + angular.forEach(model.selection, function (item) { + if (item.id === '-1') { + item.name = vm.labels.contentRoot; + item.icon = 'icon-folder'; + } + multiSelectItem(item, vm.user.startContentIds); + }); } - + // close overlay + vm.contentPicker.show = false; + vm.contentPicker = null; + }, + close: function (oldModel) { + // close overlay + vm.contentPicker.show = false; + vm.contentPicker = null; } - else { - return property.value; + }; + } + function openMediaPicker() { + vm.mediaPicker = { + title: vm.labels.selectMediaStartNode, + view: 'treepicker', + section: 'media', + treeAlias: 'media', + entityType: 'media', + multiPicker: true, + hideHeader: false, + show: true, + submit: function (model) { + // select items + if (model.selection) { + angular.forEach(model.selection, function (item) { + if (item.id === '-1') { + item.name = vm.labels.mediaRoot; + item.icon = 'icon-folder'; + } + multiSelectItem(item, vm.user.startMediaIds); + }); + } + // close overlay + vm.mediaPicker.show = false; + vm.mediaPicker = null; + }, + close: function (oldModel) { + // close overlay + vm.mediaPicker.show = false; + vm.mediaPicker = null; } + }; + } + function multiSelectItem(item, selection) { + var found = false; + // check if item is already in the selected list + if (selection.length > 0) { + angular.forEach(selection, function (selectedItem) { + if (selectedItem.udi === item.udi) { + found = true; + } + }); + } + // only add the selected item if it is not already selected + if (!found) { + selection.push(item); } - - return null; - }); - } - }); - -angular.module("umbraco").controller("Umbraco.PrevalueEditors.CropSizesController", - function ($scope, $timeout) { - - if (!$scope.model.value) { - $scope.model.value = []; - } - - $scope.remove = function (item, evt) { - evt.preventDefault(); - $scope.model.value = _.reject($scope.model.value, function (x) { - return x.alias === item.alias; - }); - }; - - $scope.edit = function (item, evt) { - evt.preventDefault(); - $scope.newItem = item; - }; - - $scope.cancel = function (evt) { - evt.preventDefault(); - $scope.newItem = null; - }; - - $scope.add = function (evt) { - evt.preventDefault(); - - if ($scope.newItem && $scope.newItem.alias && - angular.isNumber($scope.newItem.width) && angular.isNumber($scope.newItem.height) && - $scope.newItem.width > 0 && $scope.newItem.height > 0) { - - var exists = _.find($scope.model.value, function (item) { return $scope.newItem.alias === item.alias; }); - if (!exists) { - $scope.model.value.push($scope.newItem); - $scope.newItem = {}; - $scope.hasError = false; - return; - } - } - - //there was an error, do the highlight (will be set back by the directive) - $scope.hasError = true; - }; - }); -function includePropsPreValsController($rootScope, $scope, localizationService, contentTypeResource) { - - if (!$scope.model.value) { - $scope.model.value = []; - } - - $scope.propertyAliases = []; - $scope.selectedField = null; - $scope.systemFields = [ - { value: "sortOrder" }, - { value: "updateDate" }, - { value: "updater" }, - { value: "createDate" }, - { value: "owner" }, - { value: "published"}, - { value: "contentTypeAlias" }, - { value: "email" }, - { value: "username" } - ]; - - $scope.getLocalizedKey = function(alias) { - switch (alias) { - case "name": - return "general_name"; - case "sortOrder": - return "general_sort"; - case "updateDate": - return "content_updateDate"; - case "updater": - return "content_updatedBy"; - case "createDate": - return "content_createDate"; - case "owner": - return "content_createBy"; - case "published": - return "content_isPublished"; - case "contentTypeAlias": - //NOTE: This will just be 'Document' type even if it's for media/members since this is just a pre-val editor and we don't have a key for 'Content Type Alias' - return "content_documentType"; - case "email": - return "general_email"; - case "username": - return "general_username"; - } - return alias; - } - - $scope.removeField = function(e) { - $scope.model.value = _.reject($scope.model.value, function (x) { - return x.alias === e.alias; - }); - } - - //now we'll localize these strings, for some reason the directive doesn't work inside of the select group with an ng-model declared - _.each($scope.systemFields, function (e, i) { - var key = $scope.getLocalizedKey(e.value); - localizationService.localize(key).then(function (v) { - e.name = v; - - switch (e.value) { - case "updater": - e.name += " (Content only)"; - break; - case "published": - e.name += " (Content only)"; - break; - case "email": - e.name += " (Members only)"; - break; - case "username": - e.name += " (Members only)"; - break; } - - }); - }); - - // Return a helper with preserved width of cells - var fixHelper = function (e, ui) { - var h = ui.clone(); - - h.children().each(function () { - $(this).width($(this).width()); - }); - h.css("background-color", "lightgray"); - - return h; - }; - - $scope.sortableOptions = { - helper: fixHelper, - handle: ".handle", - opacity: 0.5, - axis: 'y', - containment: 'parent', - cursor: 'move', - items: '> tr', - tolerance: 'pointer', - update: function (e, ui) { - - // Get the new and old index for the moved element (using the text as the identifier) - var newIndex = ui.item.index(); - var movedAlias = $('.alias-value', ui.item).text().trim(); - var originalIndex = getAliasIndexByText(movedAlias); - - // Move the element in the model - if (originalIndex > -1) { - var movedElement = $scope.model.value[originalIndex]; - $scope.model.value.splice(originalIndex, 1); - $scope.model.value.splice(newIndex, 0, movedElement); + function removeSelectedItem(index, selection) { + selection.splice(index, 1); } - } - }; - - contentTypeResource.getAllPropertyTypeAliases().then(function(data) { - $scope.propertyAliases = data; - }); - - $scope.addField = function () { - - var val = $scope.selectedField; - var isSystem = val.startsWith("_system_"); - if (isSystem) { - val = val.trimStart("_system_"); - } - - var exists = _.find($scope.model.value, function (i) { - return i.alias === val; - }); - if (!exists) { - $scope.model.value.push({ - alias: val, - isSystem: isSystem ? 1 : 0 - }); - } - } - - function getAliasIndexByText(value) { - for (var i = 0; i < $scope.model.value.length; i++) { - if ($scope.model.value[i].alias === value) { - return i; + function disableUser() { + vm.disableUserButtonState = 'busy'; + usersResource.disableUsers([vm.user.id]).then(function (data) { + vm.user.userState = 1; + setUserDisplayState(); + vm.disableUserButtonState = 'success'; + formHelper.showNotifications(data); + }, function (error) { + vm.disableUserButtonState = 'error'; + formHelper.showNotifications(error.data); + }); } - } - - return -1; - } - -} - - -angular.module("umbraco").controller("Umbraco.PrevalueEditors.IncludePropertiesListViewController", includePropsPreValsController); -/** - * @ngdoc controller - * @name Umbraco.PrevalueEditors.ListViewLayoutsPreValsController - * @function - * - * @description - * The controller for configuring layouts for list views - */ -(function() { - "use strict"; - - function ListViewLayoutsPreValsController($scope) { - - var vm = this; - vm.focusLayoutName = false; - - vm.layoutsSortableOptions = { - distance: 10, - tolerance: "pointer", - opacity: 0.7, - scroll: true, - cursor: "move", - handle: ".list-view-layout__sort-handle" - }; - - vm.addLayout = addLayout; - vm.showPrompt = showPrompt; - vm.hidePrompt = hidePrompt; - vm.removeLayout = removeLayout; - vm.openIconPicker = openIconPicker; - - function activate() { - - - - } - - function addLayout() { - - vm.focusLayoutName = false; - - var layout = { - "name": "", - "path": "", - "icon": "icon-stop", - "selected": true - }; - - $scope.model.value.push(layout); - - } - - function showPrompt(layout) { - layout.deletePrompt = true; - } - - function hidePrompt(layout) { - layout.deletePrompt = false; - } - - function removeLayout($index, layout) { - $scope.model.value.splice($index, 1); - } - - function openIconPicker(layout) { - vm.iconPickerDialog = { - view: "iconpicker", - show: true, - submit: function(model) { - if (model.color) { - layout.icon = model.icon + " " + model.color; - } else { - layout.icon = model.icon; - } - vm.focusLayoutName = true; - vm.iconPickerDialog.show = false; - vm.iconPickerDialog = null; - } - }; - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.PrevalueEditors.ListViewLayoutsPreValsController", ListViewLayoutsPreValsController); - -})(); - -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.EditController - * @function - * - * @description - * The controller for the content type editor - */ -(function () { - "use strict"; - - function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource, $location, listViewHelper, mediaTypeHelper) { - - var vm = this; - var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; - - vm.nodeId = $scope.contentId; - // Use whitelist of allowed file types if provided - vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); - if (vm.acceptedFileTypes === '') { - // If not provided, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles - vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); - } - - vm.maxFileSize = umbracoSettings.maxFileSize + "KB"; - vm.activeDrag = false; - vm.mediaDetailsTooltip = {}; - vm.itemsWithoutFolders = []; - vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; - vm.acceptedMediatypes = []; - - vm.dragEnter = dragEnter; - vm.dragLeave = dragLeave; - vm.onFilesQueue = onFilesQueue; - vm.onUploadComplete = onUploadComplete; - - vm.hoverMediaItemDetails = hoverMediaItemDetails; - vm.selectContentItem = selectContentItem; - vm.selectItem = selectItem; - vm.selectFolder = selectFolder; - vm.goToItem = goToItem; - - function activate() { - vm.itemsWithoutFolders = filterOutFolders($scope.items); - - //no need to make another REST/DB call if this data is not used when we are browsing the bin - if ($scope.entityType === 'media' && !vm.isRecycleBin) { - mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { - vm.acceptedMediatypes = types; - }); - } - - } - - function filterOutFolders(items) { - - var newArray = []; - - if (items && items.length) { - - for (var i = 0; items.length > i; i++) { - var item = items[i]; - var isFolder = !mediaHelper.hasFilePropertyType(item); - - if (!isFolder) { - newArray.push(item); - } - } - + function enableUser() { + vm.enableUserButtonState = 'busy'; + usersResource.enableUsers([vm.user.id]).then(function (data) { + vm.user.userState = 0; + setUserDisplayState(); + vm.enableUserButtonState = 'success'; + formHelper.showNotifications(data); + }, function (error) { + vm.enableUserButtonState = 'error'; + formHelper.showNotifications(error.data); + }); } - - return newArray; - } - - function dragEnter(el, event) { - vm.activeDrag = true; - } - - function dragLeave(el, event) { - vm.activeDrag = false; - } - - function onFilesQueue() { - vm.activeDrag = false; - } - - function onUploadComplete() { - $scope.getContent($scope.contentId); - } - - function hoverMediaItemDetails(item, event, hover) { - - if (hover && !vm.mediaDetailsTooltip.show) { - - vm.mediaDetailsTooltip.event = event; - vm.mediaDetailsTooltip.item = item; - vm.mediaDetailsTooltip.show = true; - - } else if (!hover && vm.mediaDetailsTooltip.show) { - - vm.mediaDetailsTooltip.show = false; - - } - - } - - function selectContentItem(item, $event, $index) { - listViewHelper.selectHandler(item, $index, $scope.items, $scope.selection, $event); - } - - function selectItem(item, $event, $index) { - listViewHelper.selectHandler(item, $index, vm.itemsWithoutFolders, $scope.selection, $event); - } - - function selectFolder(folder, $event, $index) { - listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event); - } - - function goToItem(item, $event, $index) { - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.GridLayoutController", ListViewGridLayoutController); - -})(); - -(function () { - "use strict"; - - function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) { - - var vm = this; - var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; - - vm.nodeId = $scope.contentId; - - // Use whitelist of allowed file types if provided - vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); - if (vm.acceptedFileTypes === '') { - // If not provided, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles - vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); - } - - vm.maxFileSize = umbracoSettings.maxFileSize + "KB"; - vm.activeDrag = false; - vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; - vm.acceptedMediatypes = []; - - vm.selectItem = selectItem; - vm.clickItem = clickItem; - vm.selectAll = selectAll; - vm.isSelectedAll = isSelectedAll; - vm.isSortDirection = isSortDirection; - vm.sort = sort; - vm.dragEnter = dragEnter; - vm.dragLeave = dragLeave; - vm.onFilesQueue = onFilesQueue; - vm.onUploadComplete = onUploadComplete; - - function activate() { - - if ($scope.entityType === 'media') { - mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { - vm.acceptedMediatypes = types; + function unlockUser() { + vm.unlockUserButtonState = 'busy'; + usersResource.unlockUsers([vm.user.id]).then(function (data) { + vm.user.userState = 0; + setUserDisplayState(); + vm.unlockUserButtonState = 'success'; + formHelper.showNotifications(data); + }, function (error) { + vm.unlockUserButtonState = 'error'; + formHelper.showNotifications(error.data); }); } - - } - - function selectAll($event) { - listViewHelper.selectAllItems($scope.items, $scope.selection, $event); - } - - function isSelectedAll() { - return listViewHelper.isSelectedAll($scope.items, $scope.selection); - } - - function selectItem(selectedItem, $index, $event) { - listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); - } - - function clickItem(item) { - // if item.id is 2147483647 (int.MaxValue) use item.key - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id)); - } - - function isSortDirection(col, direction) { - return listViewHelper.setSortingDirection(col, direction, $scope.options); - } - - function sort(field, allow, isSystem) { - if (allow) { - $scope.options.orderBySystemField = isSystem; - listViewHelper.setSorting(field, allow, $scope.options); - $scope.getContent($scope.contentId); + function clearAvatar() { + // get user + usersResource.clearAvatar(vm.user.id).then(function (data) { + vm.user.avatars = data; + }); } - } - - // Dropzone upload functions - function dragEnter(el, event) { - vm.activeDrag = true; - } - - function dragLeave(el, event) { - vm.activeDrag = false; - } - - function onFilesQueue() { - vm.activeDrag = false; - } - - function onUploadComplete() { - $scope.getContent($scope.contentId); - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); - -})(); - -function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService, navigationService, treeService) { - - //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content - // that isn't created yet, if we continue this will use the parent id in the route params which isn't what - // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove - // the list view tab entirely when it's new. - if ($routeParams.create) { - $scope.isNew = true; - return; - } - - //Now we need to check if this is for media, members or content because that will depend on the resources we use - var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; - - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "member") || (appState.getSectionState("currentSection") === "member")) { - $scope.entityType = "member"; - contentResource = $injector.get('memberResource'); - getContentTypesCallback = $injector.get('memberTypeResource').getTypes; - getListResultsCallback = contentResource.getPagedResults; - deleteItemCallback = contentResource.deleteByKey; - getIdCallback = function (selected) { - var selectedKey = getItemKey(selected.id); - return selectedKey; - }; - createEditUrlCallback = function (item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; - }; - } - else { - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "media") || (appState.getSectionState("currentSection") === "media")) { - $scope.entityType = "media"; - contentResource = $injector.get('mediaResource'); - getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; - } - else { - $scope.entityType = "content"; - contentResource = $injector.get('contentResource'); - getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; - } - getListResultsCallback = contentResource.getChildren; - deleteItemCallback = contentResource.deleteById; - getIdCallback = function (selected) { - return selected.id; - }; - createEditUrlCallback = function (item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.id + "?page=" + $scope.options.pageNumber; - }; - } - - $scope.pagination = []; - $scope.isNew = false; - $scope.actionInProgress = false; - $scope.selection = []; - $scope.folders = []; - $scope.listViewResultSet = { - totalPages: 0, - items: [] - }; - - $scope.currentNodePermissions = {} - - //Just ensure we do have an editorState - if (editorState.current) { - //Fetch current node allowed actions for the current user - //This is the current node & not each individual child node in the list - var currentUserPermissions = editorState.current.allowedActions; - - //Create a nicer model rather than the funky & hard to remember permissions strings - $scope.currentNodePermissions = { - "canCopy": _.contains(currentUserPermissions, 'O'), //Magic Char = O - "canCreate": _.contains(currentUserPermissions, 'C'), //Magic Char = C - "canDelete": _.contains(currentUserPermissions, 'D'), //Magic Char = D - "canMove": _.contains(currentUserPermissions, 'M'), //Magic Char = M - "canPublish": _.contains(currentUserPermissions, 'U'), //Magic Char = U - "canUnpublish": _.contains(currentUserPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) - }; - } - - //when this is null, we don't check permissions - $scope.buttonPermissions = null; - - //When we are dealing with 'content', we need to deal with permissions on child nodes. - // Currently there is no real good way to - if ($scope.entityType === "content") { - - var idsWithPermissions = null; - - $scope.buttonPermissions = { - canCopy: true, - canCreate: true, - canDelete: true, - canMove: true, - canPublish: true, - canUnpublish: true - }; - - $scope.$watch(function () { - return $scope.selection.length; - }, function (newVal, oldVal) { - - if ((idsWithPermissions == null && newVal > 0) || (idsWithPermissions != null)) { - - //get all of the selected ids - var ids = _.map($scope.selection, function (i) { - return i.id.toString(); - }); - - //remove the dictionary items that don't have matching ids - var filtered = {}; - _.each(idsWithPermissions, function (value, key, list) { - if (_.contains(ids, key)) { - filtered[key] = value; - } - }); - idsWithPermissions = filtered; - - //find all ids that we haven't looked up permissions for - var existingIds = _.keys(idsWithPermissions); - var missingLookup = _.map(_.difference(ids, existingIds), function (i) { - return Number(i); - }); - - if (missingLookup.length > 0) { - contentResource.getPermissions(missingLookup).then(function (p) { - $scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions); - }); - } - else { - $scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions); - } - } - }); - - } - - $scope.options = { - displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, - pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, - pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, - filter: '', - orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), - orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : "desc", - orderBySystemField: true, - includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ - { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, - { alias: 'updater', header: 'Last edited by', isSystem: 1 } - ], - layout: { - layouts: $scope.model.config.layouts, - activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) - }, - allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, - allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, - allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, - allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, - allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete - }; - - // Check if selected order by field is actually custom field - for (var j = 0; j < $scope.options.includeProperties.length; j++) { - var includedProperty = $scope.options.includeProperties[j]; - if (includedProperty.alias.toLowerCase() === $scope.options.orderBy.toLowerCase()) { - $scope.options.orderBySystemField = includedProperty.isSystem === 1; - break; - } - } - - //update all of the system includeProperties to enable sorting - _.each($scope.options.includeProperties, function (e, i) { - - //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted - // to do that, we'd need to update the base query for content to include the content type alias column - // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? - if (e.alias != "contentTypeAlias") { - e.allowSorting = true; - } - - // Another special case for members, only fields on the base table (cmsMember) can be used for sorting - if (e.isSystem && $scope.entityType == "member") { - e.allowSorting = e.alias == 'username' || e.alias == 'email'; - } - - if (e.isSystem) { - //localize the header - var key = getLocalizedKey(e.alias); - localizationService.localize(key).then(function (v) { - e.header = v; - }); - } - }); - - $scope.selectLayout = function (selectedLayout) { - $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); - }; - - function showNotificationsAndReset(err, reload, successMsg) { - - //check if response is ysod - if (err.status && err.status >= 500) { - - // Open ysod overlay - $scope.ysodOverlay = { - view: "ysod", - error: err, - show: true - }; - } - - $timeout(function() { - $scope.bulkStatus = ""; - $scope.actionInProgress = false; - }, - 500); - - if (reload === true) { - $scope.reloadView($scope.contentId, true); - } - - if (err.data && angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } else if (successMsg) { - localizationService.localize("bulk_done") - .then(function(v) { - notificationsService.success(v, successMsg); - }); - } - } - - $scope.next = function (pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - $scope.goToPage = function (pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - $scope.prev = function (pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - - /*Loads the search results, based on parameters set in prev,next,sort and so on*/ - /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state - with simple values */ - - $scope.getContent = function() { - $scope.reloadView($scope.contentId, true); - } - - $scope.reloadView = function (id, reloadFolders) { - - $scope.viewLoaded = false; - - listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); - - getListResultsCallback(id, $scope.options).then(function (data) { - - $scope.actionInProgress = false; - $scope.listViewResultSet = data; - - //update all values for display - if ($scope.listViewResultSet.items) { - _.each($scope.listViewResultSet.items, function (e, index) { - setPropertyValues(e); - }); - } - - if (reloadFolders && $scope.entityType === 'media') { - //The folders aren't loaded - we only need to do this once since we're never changing node ids - mediaResource.getChildFolders($scope.contentId) - .then(function (folders) { - $scope.folders = folders; - $scope.viewLoaded = true; - }); - - } else { - $scope.viewLoaded = true; - } - - //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example - // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last - // available page and then re-load again - if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { - $scope.options.pageNumber = $scope.listViewResultSet.totalPages; - - //reload! - $scope.reloadView(id); - } - - }); - }; - - var searchListView = _.debounce(function () { - $scope.$apply(function () { - makeSearch(); - }); - }, 500); - - $scope.forceSearch = function (ev) { - //13: enter - switch (ev.keyCode) { - case 13: - makeSearch(); - break; - } - }; - - $scope.enterSearch = function () { - $scope.viewLoaded = false; - searchListView(); - }; - - function makeSearch() { - if ($scope.options.filter !== null && $scope.options.filter !== undefined) { - $scope.options.pageNumber = 1; - $scope.reloadView($scope.contentId); - } - } - - $scope.isAnythingSelected = function () { - if ($scope.selection.length === 0) { - return false; - } else { - return true; - } - }; - - $scope.selectedItemsCount = function () { - return $scope.selection.length; - }; - - $scope.clearSelection = function () { - listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); - }; - - $scope.getIcon = function (entry) { - return iconHelper.convertFromLegacyIcon(entry.icon); - }; - - function serial(selected, fn, getStatusMsg, index) { - return fn(selected, index).then(function (content) { - index++; - $scope.bulkStatus = getStatusMsg(index, selected.length); - return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; - }, function (err) { - var reload = index > 0; - showNotificationsAndReset(err, reload); - return err; - }); - } - - function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { - var selected = $scope.selection; - if (selected.length === 0) - return; - if (confirmMsg && !confirm(confirmMsg)) - return; - - $scope.actionInProgress = true; - $scope.bulkStatus = getStatusMsg(0, selected.length); - - return serial(selected, fn, getStatusMsg, 0).then(function (result) { - // executes once the whole selection has been processed - // in case of an error (caught by serial), result will be the error - if (!(result.data && angular.isArray(result.data.notifications))) - showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); - }); - } - - $scope.delete = function() { - var confirmDeleteText = ""; - - localizationService.localize("defaultdialogs_confirmdelete") - .then(function(value) { - confirmDeleteText = value; - - var attempt = - applySelected( - function(selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, - function(count, total) { - var key = (total === 1 ? "bulk_deletedItemOfItem" : "bulk_deletedItemOfItems"); - return localizationService.localize(key, [count, total]); - }, - function(total) { - var key = (total === 1 ? "bulk_deletedItem" : "bulk_deletedItems"); - return localizationService.localize(key, [total]); - }, - confirmDeleteText + "?"); - if (attempt) { - attempt.then(function() { - //executes if all is successful, let's sync the tree - var activeNode = appState.getTreeState("selectedNode"); - if (activeNode) { - navigationService.reloadNode(activeNode); - } - }); - } - }); - }; - - $scope.publish = function () { - applySelected( - function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, - function (count, total) { - var key = (total === 1 ? "bulk_publishedItemOfItem" : "bulk_publishedItemOfItems"); - return localizationService.localize(key, [count, total]); - }, - function (total) { - var key = (total === 1 ? "bulk_publishedItem" : "bulk_publishedItems"); - return localizationService.localize(key, [total]); - }); - }; - - $scope.unpublish = function() { - applySelected( - function(selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, - function(count, total) { - var key = (total === 1 ? "bulk_unpublishedItemOfItem" : "bulk_unpublishedItemOfItems"); - return localizationService.localize(key, [count, total]); - }, - function(total) { - var key = (total === 1 ? "bulk_unpublishedItem" : "bulk_unpublishedItems"); - return localizationService.localize(key, [total]); - }); - }; - - $scope.move = function() { - $scope.moveDialog = {}; - $scope.moveDialog.title = localizationService.localize("general_move"); - $scope.moveDialog.section = $scope.entityType; - $scope.moveDialog.currentNode = $scope.contentId; - $scope.moveDialog.view = "move"; - $scope.moveDialog.show = true; - - $scope.moveDialog.submit = function(model) { - - if (model.target) { - performMove(model.target); - } - - $scope.moveDialog.show = false; - $scope.moveDialog = null; - }; - - $scope.moveDialog.close = function(oldModel) { - $scope.moveDialog.show = false; - $scope.moveDialog = null; - }; - - }; - - - function performMove(target) { - - //NOTE: With the way this applySelected/serial works, I'm not sure there's a better way currently to return - // a specific value from one of the methods, so we'll have to try this way. Even though the first method - // will fire once per every node moved, the destination path will be the same and we need to use that to sync. - var newPath = null; - applySelected( - function(selected, index) { - return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }) - .then(function(path) { - newPath = path; - return path; - }); - }, - function(count, total) { - var key = (total === 1 ? "bulk_movedItemOfItem" : "bulk_movedItemOfItems"); - return localizationService.localize(key, [count, total]); - }, - function(total) { - var key = (total === 1 ? "bulk_movedItem" : "bulk_movedItems"); - return localizationService.localize(key, [total]); - }) - .then(function() { - //executes if all is successful, let's sync the tree - if (newPath) { - - //we need to do a double sync here: first refresh the node where the content was moved, - // then refresh the node where the content was moved from - navigationService.syncTree({ - tree: target.nodeType ? target.nodeType : (target.metaData.treeAlias), - path: newPath, - forceReload: true, - activate: false - }) - .then(function(args) { - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - if (activeNode) { - navigationService.reloadNode(activeNode); - } - }); - } - }); - } - - $scope.copy = function () { - $scope.copyDialog = {}; - $scope.copyDialog.title = localizationService.localize("general_copy"); - $scope.copyDialog.section = $scope.entityType; - $scope.copyDialog.currentNode = $scope.contentId; - $scope.copyDialog.view = "copy"; - $scope.copyDialog.show = true; - - $scope.copyDialog.submit = function (model) { - if (model.target) { - performCopy(model.target, model.relateToOriginal); - } - - $scope.copyDialog.show = false; - $scope.copyDialog = null; - }; - - $scope.copyDialog.close = function (oldModel) { - $scope.copyDialog.show = false; - $scope.copyDialog = null; - }; - - }; - - function performCopy(target, relateToOriginal) { - applySelected( - function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, - function (count, total) { - var key = (total === 1 ? "bulk_copiedItemOfItem" : "bulk_copiedItemOfItems"); - return localizationService.localize(key, [count, total]); - }, - function (total) { - var key = (total === 1 ? "bulk_copiedItem" : "bulk_copiedItems"); - return localizationService.localize(key, [total]); - }); - } - - function getCustomPropertyValue(alias, properties) { - var value = ''; - var index = 0; - var foundAlias = false; - for (var i = 0; i < properties.length; i++) { - if (properties[i].alias == alias) { - foundAlias = true; - break; - } - index++; - } - - if (foundAlias) { - value = properties[index].value; - } - - return value; - } - - /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ - function setPropertyValues(result) { - - //set the edit url - result.editPath = createEditUrlCallback(result); - - _.each($scope.options.includeProperties, function (e, i) { - - var alias = e.alias; - - // First try to pull the value directly from the alias (e.g. updatedBy) - var value = result[alias]; - - // If this returns an object, look for the name property of that (e.g. owner.name) - if (value === Object(value)) { - value = value['name']; - } - - // If we've got nothing yet, look at a user defined property - if (typeof value === 'undefined') { - value = getCustomPropertyValue(alias, result.properties); - } - - // If we have a date, format it - if (isDate(value)) { - value = value.substring(0, value.length - 3); - } - - // set what we've got on the result - result[alias] = value; - }); - - - } - - function isDate(val) { - if (angular.isString(val)) { - return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); - } - return false; - } - - function initView() { - //default to root id if the id is undefined - var id = $routeParams.id; - if (id === undefined) { - id = -1; - } - - $scope.listViewAllowedTypes = getContentTypesCallback(id); - - $scope.contentId = id; - $scope.isTrashed = id === "-20" || id === "-21"; - - $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; - $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; - - $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || - $scope.options.allowBulkUnpublish || - $scope.options.allowBulkCopy || - $scope.options.allowBulkMove || - $scope.options.allowBulkDelete; - - $scope.reloadView($scope.contentId, true); - } - - function getLocalizedKey(alias) { - - switch (alias) { - case "sortOrder": - return "general_sort"; - case "updateDate": - return "content_updateDate"; - case "updater": - return "content_updatedBy"; - case "createDate": - return "content_createDate"; - case "owner": - return "content_createBy"; - case "published": - return "content_isPublished"; - case "contentTypeAlias": - //TODO: Check for members - return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; - case "email": - return "general_email"; - case "username": - return "general_username"; - } - return alias; - } - - function getItemKey(itemId) { - for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { - var item = $scope.listViewResultSet.items[i]; - if (item.id === itemId) { - return item.key; - } - } - } - - //GO! - initView(); -} - - -angular.module("umbraco").controller("Umbraco.PropertyEditors.ListViewController", listViewController); - -function sortByPreValsController($rootScope, $scope, localizationService, editorState, listViewPrevalueHelper) { - //Get the prevalue from the correct place - function getPrevalues() { - if (editorState.current.preValues) { - return editorState.current.preValues; - } - else { - return listViewPrevalueHelper.getPrevalues(); - } - } - - //Watch the prevalues - $scope.$watch(function () { - return _.findWhere(getPrevalues(), { key: "includeProperties" }).value; - }, function () { - populateFields(); - }, true); //Use deep watching, otherwise we won't pick up header changes - - function populateFields() { - // Helper to find a particular value from the list of sort by options - function findFromSortByFields(value) { - return _.find($scope.sortByFields, function (e) { - return e.value.toLowerCase() === value.toLowerCase(); - }); - } - - // Get list of properties assigned as columns of the list view - var propsPreValue = _.findWhere(getPrevalues(), { key: "includeProperties" }); - - // Populate list of options for the default sort (all the columns plus then node name) - $scope.sortByFields = []; - $scope.sortByFields.push({ value: "name", name: "Name", isSystem: 1 }); - if (propsPreValue != undefined) { - for (var i = 0; i < propsPreValue.value.length; i++) { - var value = propsPreValue.value[i]; - $scope.sortByFields.push({ - value: value.alias, - name: value.header, - isSystem: value.isSystem - }); - } - } - - // Localize the system fields, for some reason the directive doesn't work inside of the select group with an ng-model declared - var systemFields = [ - { value: "SortOrder", key: "general_sort" }, - { value: "Name", key: "general_name" }, - { value: "VersionDate", key: "content_updateDate" }, - { value: "Updater", key: "content_updatedBy" }, - { value: "CreateDate", key: "content_createDate" }, - { value: "Owner", key: "content_createBy" }, - { value: "ContentTypeAlias", key: "content_documentType" }, - { value: "Published", key: "content_isPublished" }, - { value: "Email", key: "general_email" }, - { value: "Username", key: "general_username" } - ]; - _.each(systemFields, function (e) { - localizationService.localize(e.key).then(function (v) { - - var sortByListValue = findFromSortByFields(e.value); - if (sortByListValue) { - sortByListValue.name = v; - switch (e.value) { - case "Updater": - e.name += " (Content only)"; - break; - case "Published": - e.name += " (Content only)"; - break; - case "Email": - e.name += " (Members only)"; - break; - case "Username": - e.name += " (Members only)"; - break; - } + $scope.changeAvatar = function (files, event) { + if (files && files.length > 0) { + upload(files[0]); } - }); - }); - - // Check existing model value is available in list and ensure a value is set - var existingValue = findFromSortByFields($scope.model.value); - if (existingValue) { - // Set the existing value - // The old implementation pre Umbraco 7.5 used PascalCase aliases, this uses camelCase, so this ensures that any previous value is set - $scope.model.value = existingValue.value; - } - else { - // Existing value not found, set to first value - $scope.model.value = $scope.sortByFields[0].value; - } - } -} - - -angular.module("umbraco").controller("Umbraco.PrevalueEditors.SortByListViewController", sortByPreValsController); -//DO NOT DELETE THIS, this is in use... -angular.module('umbraco') -.controller("Umbraco.PropertyEditors.MacroContainerController", - - function($scope, dialogService, entityResource, macroService){ - - $scope.renderModel = []; - $scope.allowOpenButton = true; - $scope.allowRemoveButton = true; - $scope.sortableOptions = {}; - - if($scope.model.value){ - var macros = $scope.model.value.split('>'); - - angular.forEach(macros, function(syntax, key){ - if(syntax && syntax.length > 10){ - //re-add the char we split on - syntax = syntax + ">"; - var parsed = macroService.parseMacroSyntax(syntax); - if(!parsed){ - parsed = {}; - } - - parsed.syntax = syntax; - collectDetails(parsed); - $scope.renderModel.push(parsed); - setSortingState($scope.renderModel); - } - }); - } - - - function collectDetails(macro){ - macro.details = ""; - macro.icon = "icon-settings-alt"; - if(macro.macroParamsDictionary){ - angular.forEach((macro.macroParamsDictionary), function(value, key){ - macro.details += key + ": " + value + " "; - }); - } - } - - function openDialog(index){ - var dialogData = { - allowedMacros: $scope.model.config.allowed - }; - - if(index !== null && $scope.renderModel[index]) { - var macro = $scope.renderModel[index]; - dialogData["macroData"] = macro; - } - - $scope.macroPickerOverlay = {}; - $scope.macroPickerOverlay.view = "macropicker"; - $scope.macroPickerOverlay.dialogData = dialogData; - $scope.macroPickerOverlay.show = true; - - $scope.macroPickerOverlay.submit = function(model) { - - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); - collectDetails(macroObject); - - //update the raw syntax and the list... - if(index !== null && $scope.renderModel[index]) { - $scope.renderModel[index] = macroObject; - } else { - $scope.renderModel.push(macroObject); - } - - setSortingState($scope.renderModel); - - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - $scope.macroPickerOverlay.close = function(oldModel) { - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - } - - - - $scope.edit =function(index){ - openDialog(index); - }; - - $scope.add = function () { - - if ($scope.model.config.max && $scope.model.config.max > 0 && $scope.renderModel.length >= $scope.model.config.max) { - //cannot add more than the max - return; - } - - openDialog(); - }; - - $scope.remove =function(index){ - $scope.renderModel.splice(index, 1); - setSortingState($scope.renderModel); - }; - - $scope.clear = function() { - $scope.model.value = ""; - $scope.renderModel = []; - }; - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var syntax = []; - angular.forEach($scope.renderModel, function(value, key){ - syntax.push(value.syntax); - }); - - $scope.model.value = syntax.join(""); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - - function trim(str, chr) { - var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^'+chr+'+|'+chr+'+$', 'g'); - return str.replace(rgxtrim, ''); - } - - function setSortingState(items) { - // disable sorting if the list only consist of one item - if(items.length > 1) { - $scope.sortableOptions.disabled = false; - } else { - $scope.sortableOptions.disabled = true; - } - } - -}); - -function MacroListController($scope, entityResource) { - - $scope.items = []; - - entityResource.getAll("Macro").then(function(items) { - _.each(items, function(i) { - $scope.items.push({ name: i.name, alias: i.alias }); - }); - - }); - - -} - -angular.module("umbraco").controller("Umbraco.PrevalueEditors.MacroList", MacroListController); - -//inject umbracos assetsServce and dialog service -function MarkdownEditorController($scope, $element, assetsService, dialogService, angularHelper, $timeout) { - - //tell the assets service to load the markdown.editor libs from the markdown editors - //plugin folder - - if ($scope.model.value === null || $scope.model.value === "") { - $scope.model.value = $scope.model.config.defaultValue; - } - - function openMediaPicker(callback) { - - $scope.mediaPickerOverlay = {}; - $scope.mediaPickerOverlay.view = "mediaPicker"; - $scope.mediaPickerOverlay.show = true; - $scope.mediaPickerOverlay.disableFolderSelect = true; - - $scope.mediaPickerOverlay.submit = function(model) { - - var selectedImagePath = model.selectedImages[0].image; - callback(selectedImagePath); - - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - }; - - $scope.mediaPickerOverlay.close = function(model) { - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - }; - - } - - assetsService - .load([ - "lib/markdown/markdown.converter.js", - "lib/markdown/markdown.sanitizer.js", - "lib/markdown/markdown.editor.js" - ]) - .then(function () { - - // we need a short delay to wait for the textbox to appear. - setTimeout(function () { - //this function will execute when all dependencies have loaded - // but in the case that they've been previously loaded, we can only - // init the md editor after this digest because the DOM needs to be ready first - // so run the init on a timeout - $timeout(function () { - var converter2 = new Markdown.Converter(); - var editor2 = new Markdown.Editor(converter2, "-" + $scope.model.alias); - editor2.run(); - - //subscribe to the image dialog clicks - editor2.hooks.set("insertImageDialog", function (callback) { - openMediaPicker(callback); - return true; // tell the editor that we'll take care of getting the image url - }); - - editor2.hooks.set("onPreviewRefresh", function () { - // We must manually update the model as there is no way to hook into the markdown editor events without exstensive edits to the library. - if ($scope.model.value !== $("textarea", $element).val()) { - angularHelper.getCurrentForm($scope).$setDirty(); - $scope.model.value = $("textarea", $element).val(); - } - }); - - }, 200); - }); - - //load the seperat css for the editor to avoid it blocking our js loading TEMP HACK - assetsService.loadCss("lib/markdown/markdown.css"); - }) -} - -angular.module("umbraco").controller("Umbraco.PropertyEditors.MarkdownEditorController", MarkdownEditorController); - -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerController", - function ($rootScope, $scope, dialogService, entityResource, mediaResource, mediaHelper, $timeout, userService, $location) { - - //check the pre-values for multi-picker - var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; - var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false; - var disableFolderSelect = $scope.model.config.disableFolderSelect && $scope.model.config.disableFolderSelect !== '0' ? true : false; - - if (!$scope.model.config.startNodeId) { - userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaId; - }); - } - - function setupViewModel() { - $scope.images = []; - $scope.ids = []; - - if ($scope.model.value) { - var ids = $scope.model.value.split(','); - - //NOTE: We need to use the entityResource NOT the mediaResource here because - // the mediaResource has server side auth configured for which the user must have - // access to the media section, if they don't they'll get auth errors. The entityResource - // acts differently in that it allows access if the user has access to any of the apps that - // might require it's use. Therefore we need to use the metatData property to get at the thumbnail - // value. - - entityResource.getByIds(ids, "Media").then(function (medias) { - - _.each(medias, function (media, i) { - - //only show non-trashed items - if (media.parentId >= -1) { - - if (!media.thumbnail) { - media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); - } - - $scope.images.push(media); - - if ($scope.model.config.idType === "udi") { - $scope.ids.push(media.udi); - } - else { - $scope.ids.push(media.id); - } - } - }); - - $scope.sync(); - }); - } - } - - setupViewModel(); - - $scope.remove = function(index) { - $scope.images.splice(index, 1); - $scope.ids.splice(index, 1); - $scope.sync(); - }; - - $scope.goToItem = function(item) { - $location.path('media/media/edit/' + item.id); - }; - - $scope.add = function() { - - $scope.mediaPickerOverlay = { - view: "mediapicker", - title: "Select media", - startNodeId: $scope.model.config.startNodeId, - multiPicker: multiPicker, - onlyImages: onlyImages, - disableFolderSelect: disableFolderSelect, - show: true, - submit: function(model) { - - _.each(model.selectedImages, function(media, i) { - - if (!media.thumbnail) { - media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); - } - - $scope.images.push(media); - - if ($scope.model.config.idType === "udi") { - $scope.ids.push(media.udi); - } - else { - $scope.ids.push(media.id); - } - }); - - $scope.sync(); - - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - - } - }; - - }; - - $scope.sortableOptions = { - update: function(e, ui) { - var r = []; - //TODO: Instead of doing this with a half second delay would be better to use a watch like we do in the - // content picker. THen we don't have to worry about setting ids, render models, models, we just set one and let the - // watch do all the rest. - $timeout(function(){ - angular.forEach($scope.images, function(value, key) { - r.push($scope.model.config.idType === "udi" ? value.udi : value.id); - }); - $scope.ids = r; - $scope.sync(); - }, 500, false); - } - }; - - $scope.sync = function() { - $scope.model.value = $scope.ids.join(); - }; - - $scope.showAdd = function () { - if (!multiPicker) { - if ($scope.model.value && $scope.model.value !== "") { - return false; - } - } - return true; - }; - - //here we declare a special method which will be called whenever the value has changed from the server - //this is instead of doing a watch on the model.value = faster - $scope.model.onValueChanged = function (newVal, oldVal) { - //update the display val again if it has changed from the server - setupViewModel(); - }; - - }); - -//this controller simply tells the dialogs service to open a memberPicker window -//with a specified callback, this callback will receive an object with a selection on it -function memberGroupPicker($scope, dialogService){ - - function trim(str, chr) { - var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); - return str.replace(rgxtrim, ''); - } - - $scope.renderModel = []; - $scope.allowRemove = true; - - if ($scope.model.value) { - var modelIds = $scope.model.value.split(','); - _.each(modelIds, function (item, i) { - $scope.renderModel.push({ name: item, id: item, icon: 'icon-users' }); - }); - } - - $scope.openMemberGroupPicker = function() { - - $scope.memberGroupPicker = {}; - $scope.memberGroupPicker.multiPicker = true; - $scope.memberGroupPicker.view = "memberGroupPicker"; - $scope.memberGroupPicker.show = true; - - $scope.memberGroupPicker.submit = function(model) { - - if(model.selectedMemberGroups) { - _.each(model.selectedMemberGroups, function (item, i) { - $scope.add(item); - }); - } - - if(model.selectedMemberGroup) { - $scope.clear(); - $scope.add(model.selectedMemberGroup); - } - - $scope.memberGroupPicker.show = false; - $scope.memberGroupPicker = null; - }; - - $scope.memberGroupPicker.close = function(oldModel) { - $scope.memberGroupPicker.show = false; - $scope.memberGroupPicker = null; - }; - - }; - - $scope.remove =function(index){ - $scope.renderModel.splice(index, 1); - }; - - $scope.add = function (item) { - var currIds = _.map($scope.renderModel, function (i) { - return i.id; - }); - - if (currIds.indexOf(item) < 0) { - $scope.renderModel.push({ name: item, id: item, icon: 'icon-users' }); - } - }; - - $scope.clear = function() { - $scope.renderModel = []; - }; - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var currIds = _.map($scope.renderModel, function (i) { - return i.id; - }); - $scope.model.value = trim(currIds.join(), ","); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - -} - -angular.module('umbraco').controller("Umbraco.PropertyEditors.MemberGroupPickerController", memberGroupPicker); - -function memberGroupController($rootScope, $scope, dialogService, mediaResource, imageHelper, $log) { - - //set the available to the keys of the dictionary who's value is true - $scope.getAvailable = function () { - var available = []; - for (var n in $scope.model.value) { - if ($scope.model.value[n] === false) { - available.push(n); - } - } - return available; - }; - //set the selected to the keys of the dictionary who's value is true - $scope.getSelected = function () { - var selected = []; - for (var n in $scope.model.value) { - if ($scope.model.value[n] === true) { - selected.push(n); - } - } - return selected; - }; - - $scope.addItem = function(item) { - //keep the model up to date - $scope.model.value[item] = true; - }; - - $scope.removeItem = function (item) { - //keep the model up to date - $scope.model.value[item] = false; - }; - - -} -angular.module('umbraco').controller("Umbraco.PropertyEditors.MemberGroupController", memberGroupController); -//this controller simply tells the dialogs service to open a memberPicker window -//with a specified callback, this callback will receive an object with a selection on it -function memberPickerController($scope, dialogService, entityResource, $log, iconHelper, angularHelper){ - - function trim(str, chr) { - var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); - return str.replace(rgxtrim, ''); - } - - $scope.renderModel = []; - $scope.allowRemove = true; - - var dialogOptions = { - multiPicker: false, - entityType: "Member", - section: "member", - treeAlias: "member", - filter: function(i) { - return i.metaData.isContainer == true; - }, - filterCssClass: "not-allowed", - callback: function(data) { - if (angular.isArray(data)) { - _.each(data, function (item, i) { - $scope.add(item); - }); - } else { - $scope.clear(); - $scope.add(data); - } - angularHelper.getCurrentForm($scope).$setDirty(); - } - }; - - //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the - // pre-value config on to the dialog options - if ($scope.model.config) { - angular.extend(dialogOptions, $scope.model.config); - } - - $scope.openMemberPicker = function() { - $scope.memberPickerOverlay = dialogOptions; - $scope.memberPickerOverlay.view = "memberPicker"; - $scope.memberPickerOverlay.show = true; - - $scope.memberPickerOverlay.submit = function(model) { - - if (model.selection) { - _.each(model.selection, function(item, i) { - $scope.add(item); - }); - } - - $scope.memberPickerOverlay.show = false; - $scope.memberPickerOverlay = null; - }; - - $scope.memberPickerOverlay.close = function(oldModel) { - $scope.memberPickerOverlay.show = false; - $scope.memberPickerOverlay = null; - }; - - }; - - $scope.remove =function(index){ - $scope.renderModel.splice(index, 1); - }; - - $scope.add = function (item) { - var currIds = _.map($scope.renderModel, function (i) { - if ($scope.model.config.idType === "udi") { - return i.udi; - } - else { - return i.id; - } - }); - - var itemId = $scope.model.config.idType === "udi" ? item.udi : item.id; - - if (currIds.indexOf(itemId) < 0) { - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon}); - } - }; - - $scope.clear = function() { - $scope.renderModel = []; - }; - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var currIds = _.map($scope.renderModel, function (i) { - if ($scope.model.config.idType === "udi") { - return i.udi; - } - else { - return i.id; - } - }); - $scope.model.value = trim(currIds.join(), ","); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - //load member data - var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; - entityResource.getByIds(modelIds, "Member").then(function (data) { - _.each(data, function (item, i) { - // set default icon if it's missing - item.icon = (item.icon) ? iconHelper.convertFromLegacyIcon(item.icon) : "icon-user"; - $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon }); - }); - }); -} - - -angular.module('umbraco').controller("Umbraco.PropertyEditors.MemberPickerController", memberPickerController); - -function MultipleTextBoxController($scope) { - - $scope.sortableOptions = { - axis: 'y', - containment: 'parent', - cursor: 'move', - items: '> div.control-group', - tolerance: 'pointer' - }; - - if (!$scope.model.value) { - $scope.model.value = []; - } - - //add any fields that there isn't values for - if ($scope.model.config.min > 0) { - for (var i = 0; i < $scope.model.config.min; i++) { - if ((i + 1) > $scope.model.value.length) { - $scope.model.value.push({ value: "" }); - } - } - } - - $scope.add = function () { - if ($scope.model.config.max <= 0 || $scope.model.value.length < $scope.model.config.max) { - $scope.model.value.push({ value: "" }); - } - }; - - $scope.remove = function(index) { - var remainder = []; - for (var x = 0; x < $scope.model.value.length; x++) { - if (x !== index) { - remainder.push($scope.model.value[x]); - } - } - $scope.model.value = remainder; - }; - -} - -angular.module("umbraco").controller("Umbraco.PropertyEditors.MultipleTextBoxController", MultipleTextBoxController); - -angular.module("umbraco").controller("Umbraco.PropertyEditors.RadioButtonsController", - function($scope) { - - if (angular.isObject($scope.model.config.items)) { - - //now we need to format the items in the dictionary because we always want to have an array - var newItems = []; - var vals = _.values($scope.model.config.items); - var keys = _.keys($scope.model.config.items); - for (var i = 0; i < vals.length; i++) { - newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: vals[i].value }); + }; + function upload(file) { + vm.avatarFile.uploadProgress = 0; + Upload.upload({ + url: umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostSetAvatar', { id: vm.user.id }), + fields: {}, + file: file + }).progress(function (evt) { + if (vm.avatarFile.uploadStatus !== 'done' && vm.avatarFile.uploadStatus !== 'error') { + // set uploading status on file + vm.avatarFile.uploadStatus = 'uploading'; + // calculate progress in percentage + var progressPercentage = parseInt(100 * evt.loaded / evt.total, 10); + // set percentage property on file + vm.avatarFile.uploadProgress = progressPercentage; + } + }).success(function (data, status, headers, config) { + // set done status on file + vm.avatarFile.uploadStatus = 'done'; + vm.avatarFile.uploadProgress = 100; + vm.user.avatars = data; + }).error(function (evt, status, headers, config) { + // set status done + vm.avatarFile.uploadStatus = 'error'; + // If file not found, server will return a 404 and display this message + if (status === 404) { + vm.avatarFile.serverErrorMessage = 'File not found'; + } else if (status == 400) { + //it's a validation error + vm.avatarFile.serverErrorMessage = evt.message; + } else { + //it's an unhandled error + //if the service returns a detailed error + if (evt.InnerException) { + vm.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage; + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf('ValidateRequestEntityLength') > 0) { + vm.avatarFile.serverErrorMessage = 'File too large to upload'; + } + } else if (evt.Message) { + vm.avatarFile.serverErrorMessage = evt.Message; + } + } + }); } - - //ensure the items are sorted by the provided sort order - newItems.sort(function (a, b) { return (a.sortOrder > b.sortOrder) ? 1 : ((b.sortOrder > a.sortOrder) ? -1 : 0); }); - - //re-assign - $scope.model.config.items = newItems; - - } - - }); - -/** - * @ngdoc controller - * @name Umbraco.Editors.ReadOnlyValueController - * @function - * - * @description - * The controller for the readonlyvalue property editor. - * This controller offer more functionality than just a simple label as it will be able to apply formatting to the - * value to be displayed. This means that we also have to apply more complex logic of watching the model value when - * it changes because we are creating a new scope value called displayvalue which will never change based on the server data. - * In some cases after a form submission, the server will modify the data that has been persisted, especially in the cases of - * readonlyvalues so we need to ensure that after the form is submitted that the new data is reflected here. -*/ -function ReadOnlyValueController($rootScope, $scope, $filter) { - - function formatDisplayValue() { - - if ($scope.model.config && - angular.isArray($scope.model.config) && - $scope.model.config.length > 0 && - $scope.model.config[0] && - $scope.model.config.filter) { - - if ($scope.model.config.format) { - $scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value, $scope.model.config.format); - } else { - $scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value); - } - } else { - $scope.displayvalue = $scope.model.value; - } - - } - - //format the display value on init: - formatDisplayValue(); - - $scope.$watch("model.value", function (newVal, oldVal) { - //cannot just check for !newVal because it might be an empty string which we - //want to look for. - if (newVal !== null && newVal !== undefined && newVal !== oldVal) { - //update the display val again - formatDisplayValue(); - } - }); -} - -angular.module('umbraco').controller("Umbraco.PropertyEditors.ReadOnlyValueController", ReadOnlyValueController); -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.RelatedLinksController", - function ($rootScope, $scope, dialogService, iconHelper) { - - if (!$scope.model.value) { - $scope.model.value = []; - } - - $scope.model.config.max = isNumeric($scope.model.config.max) && $scope.model.config.max !== 0 ? $scope.model.config.max : Number.MAX_VALUE; - - $scope.newCaption = ''; - $scope.newLink = 'http://'; - $scope.newNewWindow = false; - $scope.newInternal = null; - $scope.newInternalName = ''; - $scope.newInternalIcon = null; - $scope.addExternal = true; - $scope.currentEditLink = null; - $scope.hasError = false; - - $scope.internal = function($event) { - $scope.currentEditLink = null; - - $scope.contentPickerOverlay = {}; - $scope.contentPickerOverlay.view = "contentpicker"; - $scope.contentPickerOverlay.multiPicker = false; - $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : "int"; - - $scope.contentPickerOverlay.submit = function(model) { - - select(model.selection[0]); - - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - }; - - $scope.contentPickerOverlay.close = function(oldModel) { - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - }; - - $event.preventDefault(); - }; - - $scope.selectInternal = function ($event, link) { - $scope.currentEditLink = link; - - $scope.contentPickerOverlay = {}; - $scope.contentPickerOverlay.view = "contentpicker"; - $scope.contentPickerOverlay.multiPicker = false; - $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : "int"; - - $scope.contentPickerOverlay.submit = function(model) { - - select(model.selection[0]); - - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - }; - - $scope.contentPickerOverlay.close = function(oldModel) { - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - }; - - $event.preventDefault(); - - }; - - $scope.edit = function (idx) { - for (var i = 0; i < $scope.model.value.length; i++) { - $scope.model.value[i].edit = false; - } - $scope.model.value[idx].edit = true; - }; - - $scope.saveEdit = function (idx) { - $scope.model.value[idx].title = $scope.model.value[idx].caption; - $scope.model.value[idx].edit = false; - }; - - $scope.delete = function (idx) { - $scope.model.value.splice(idx, 1); - }; - - $scope.add = function ($event) { - if ($scope.newCaption == "") { - $scope.hasError = true; - } else { - if ($scope.addExternal) { - var newExtLink = new function() { - this.caption = $scope.newCaption; - this.link = $scope.newLink; - this.newWindow = $scope.newNewWindow; - this.edit = false; - this.isInternal = false; - this.type = "external"; - this.title = $scope.newCaption; - }; - $scope.model.value.push(newExtLink); - } else { - var newIntLink = new function() { - this.caption = $scope.newCaption; - this.link = $scope.newInternal; - this.newWindow = $scope.newNewWindow; - this.internal = $scope.newInternal; - this.edit = false; - this.isInternal = true; - this.internalName = $scope.newInternalName; - this.internalIcon = $scope.newInternalIcon; - this.type = "internal"; - this.title = $scope.newCaption; - }; - $scope.model.value.push(newIntLink); - } - $scope.newCaption = ''; - $scope.newLink = 'http://'; - $scope.newNewWindow = false; - $scope.newInternal = null; - $scope.newInternalName = ''; - $scope.newInternalIcon = null; - } - $event.preventDefault(); - }; - - $scope.switch = function ($event) { - $scope.addExternal = !$scope.addExternal; - $event.preventDefault(); - }; - - $scope.switchLinkType = function ($event, link) { - link.isInternal = !link.isInternal; - link.type = link.isInternal ? "internal" : "external"; - if (!link.isInternal) - link.link = $scope.newLink; - $event.preventDefault(); - }; - - $scope.move = function (index, direction) { - var temp = $scope.model.value[index]; - $scope.model.value[index] = $scope.model.value[index + direction]; - $scope.model.value[index + direction] = temp; - }; - - //helper for determining if a user can add items - $scope.canAdd = function () { - return $scope.model.config.max <= 0 || $scope.model.config.max > countVisible(); - } - - //helper that returns if an item can be sorted - $scope.canSort = function () { - return countVisible() > 1; - } - - $scope.sortableOptions = { - axis: 'y', - handle: '.handle', - cursor: 'move', - cancel: '.no-drag', - containment: 'parent', - placeholder: 'sortable-placeholder', - forcePlaceholderSize: true, - helper: function (e, ui) { - // When sorting table rows, the cells collapse. This helper fixes that: http://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/ - ui.children().each(function () { - $(this).width($(this).width()); - }); - return ui; - }, - items: '> tr:not(.unsortable)', - tolerance: 'pointer', - update: function (e, ui) { - // Get the new and old index for the moved element (using the URL as the identifier) - var newIndex = ui.item.index(); - var movedLinkUrl = ui.item.attr('data-link'); - var originalIndex = getElementIndexByUrl(movedLinkUrl); - - // Move the element in the model - var movedElement = $scope.model.value[originalIndex]; - $scope.model.value.splice(originalIndex, 1); - $scope.model.value.splice(newIndex, 0, movedElement); - }, - start: function (e, ui) { - //ui.placeholder.html(""); - - // Build a placeholder cell that spans all the cells in the row: http://stackoverflow.com/questions/25845310/jquery-ui-sortable-and-table-cell-size - var cellCount = 0; - $('td, th', ui.helper).each(function () { - // For each td or th try and get it's colspan attribute, and add that or 1 to the total - var colspan = 1; - var colspanAttr = $(this).attr('colspan'); - if (colspanAttr > 1) { - colspan = colspanAttr; - } - cellCount += colspan; - }); - - // Add the placeholder UI - note that this is the item's content, so td rather than tr - and set height of tr - ui.placeholder.html('').height(ui.item.height()); - } - }; - - //helper to count what is visible - function countVisible() { - return $scope.model.value.length; - } - - function isNumeric(n) { - return !isNaN(parseFloat(n)) && isFinite(n); - } - - function getElementIndexByUrl(url) { - for (var i = 0; i < $scope.model.value.length; i++) { - if ($scope.model.value[i].link == url) { - return i; - } - } - - return -1; - } - - function select(data) { - if ($scope.currentEditLink != null) { - $scope.currentEditLink.internal = $scope.model.config.idType === "udi" ? data.udi : data.id; - $scope.currentEditLink.internalName = data.name; - $scope.currentEditLink.internalIcon = iconHelper.convertFromLegacyIcon(data.icon); - $scope.currentEditLink.link = $scope.model.config.idType === "udi" ? data.udi : data.id; - } else { - $scope.newInternal = $scope.model.config.idType === "udi" ? data.udi : data.id; - $scope.newInternalName = data.name; - $scope.newInternalIcon = iconHelper.convertFromLegacyIcon(data.icon); - } - } - }); - -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.RTEController", - function ($rootScope, $scope, $q, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource, macroService) { - - $scope.isLoading = true; - - //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias - // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because - // we have this mini content editor panel that can be launched with MNTP. - var d = new Date(); - var n = d.getTime(); - $scope.textAreaHtmlId = $scope.model.alias + "_" + n + "_rte"; - - var alreadyDirty = false; - function syncContent(editor){ - editor.save(); - angularHelper.safeApply($scope, function () { - $scope.model.value = editor.getContent(); - }); - - if (!alreadyDirty) { - //make the form dirty manually so that the track changes works, setting our model doesn't trigger - // the angular bits because tinymce replaces the textarea. - var currForm = angularHelper.getCurrentForm($scope); - currForm.$setDirty(); - alreadyDirty = true; - } - } - - tinyMceService.configuration().then(function (tinyMceConfig) { - - //config value from general tinymce.config file - var validElements = tinyMceConfig.validElements; - - //These are absolutely required in order for the macros to render inline - //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce - var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],span[id|class|style]"; - - var invalidElements = tinyMceConfig.inValidElements; - var plugins = _.map(tinyMceConfig.plugins, function (plugin) { - if (plugin.useOnFrontend) { - return plugin.name; - } - }).join(" "); - - var editorConfig = $scope.model.config.editor; - if (!editorConfig || angular.isString(editorConfig)) { - editorConfig = tinyMceService.defaultPrevalues(); - } - - //config value on the data type - var toolbar = editorConfig.toolbar.join(" | "); - var stylesheets = []; - var styleFormats = []; - var await = []; - if (!editorConfig.maxImageSize && editorConfig.maxImageSize != 0) { - editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize; - } - - //queue file loading - if (typeof tinymce === "undefined") { // Don't reload tinymce if already loaded - await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", $scope)); - } - - //queue rules loading - angular.forEach(editorConfig.stylesheets, function (val, key) { - stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css?" + new Date().getTime()); - await.push(stylesheetResource.getRulesByName(val).then(function (rules) { - angular.forEach(rules, function (rule) { - var r = {}; - r.title = rule.name; - if (rule.selector[0] == ".") { - r.inline = "span"; - r.classes = rule.selector.substring(1); - } - else if (rule.selector[0] == "#") { - r.inline = "span"; - r.attributes = { id: rule.selector.substring(1) }; - } - else if (rule.selector[0] != "." && rule.selector.indexOf(".") > -1) { - var split = rule.selector.split("."); - r.block = split[0]; - r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(".", " "); - } - else if (rule.selector[0] != "#" && rule.selector.indexOf("#") > -1) { - var split = rule.selector.split("#"); - r.block = split[0]; - r.classes = rule.selector.substring(rule.selector.indexOf("#") + 1); - } - else { - r.block = rule.selector; - } - - styleFormats.push(r); - }); - })); - }); - - - //stores a reference to the editor - var tinyMceEditor = null; - - //wait for queue to end - $q.all(await).then(function () { - - //create a baseline Config to exten upon - var baseLineConfigObj = { - mode: "exact", - skin: "umbraco", - plugins: plugins, - valid_elements: validElements, - invalid_elements: invalidElements, - extended_valid_elements: extendedValidElements, - menubar: false, - statusbar: false, - height: editorConfig.dimensions.height, - width: editorConfig.dimensions.width, - maxImageSize: editorConfig.maxImageSize, - toolbar: toolbar, - content_css: stylesheets, - relative_urls: false, - style_formats: styleFormats - }; - - - if (tinyMceConfig.customConfig) { - - //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to - // convert it to json instead of having it as a string since this is what tinymce requires - for (var i in tinyMceConfig.customConfig) { - var val = tinyMceConfig.customConfig[i]; - if (val) { - val = val.toString().trim(); - if (val.detectIsJson()) { - try { - tinyMceConfig.customConfig[i] = JSON.parse(val); - //now we need to check if this custom config key is defined in our baseline, if it is we don't want to - //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise - //if it's an object it will overwrite the baseline - if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { - //concat it and below this concat'd array will overwrite the baseline in angular.extend - tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]); - } - } - catch (e) { - //cannot parse, we'll just leave it - } - } - } - } - - angular.extend(baseLineConfigObj, tinyMceConfig.customConfig); - } - - //set all the things that user configs should not be able to override - baseLineConfigObj.elements = $scope.textAreaHtmlId; //this is the exact textarea id to replace! - baseLineConfigObj.setup = function (editor) { - - //set the reference - tinyMceEditor = editor; - - //enable browser based spell checking - editor.on('init', function (e) { - editor.getBody().setAttribute('spellcheck', true); - }); - - //We need to listen on multiple things here because of the nature of tinymce, it doesn't - //fire events when you think! - //The change event doesn't fire when content changes, only when cursor points are changed and undo points - //are created. the blur event doesn't fire if you insert content into the editor with a button and then - //press save. - //We have a couple of options, one is to do a set timeout and check for isDirty on the editor, or we can - //listen to both change and blur and also on our own 'saving' event. I think this will be best because a - //timer might end up using unwanted cpu and we'd still have to listen to our saving event in case they clicked - //save before the timeout elapsed. - - //TODO: We need to re-enable something like this to ensure the track changes is working with tinymce - // so we can detect if the form is dirty or not, Per has some better events to use as this one triggers - // even if you just enter/exit with mouse cursuor which doesn't really mean it's changed. - // see: http://issues.umbraco.org/issue/U4-4485 - //var alreadyDirty = false; - //editor.on('change', function (e) { - // angularHelper.safeApply($scope, function () { - // $scope.model.value = editor.getContent(); - - // if (!alreadyDirty) { - // //make the form dirty manually so that the track changes works, setting our model doesn't trigger - // // the angular bits because tinymce replaces the textarea. - // var currForm = angularHelper.getCurrentForm($scope); - // currForm.$setDirty(); - // alreadyDirty = true; - // } - - // }); - //}); - - //when we leave the editor (maybe) - editor.on('blur', function (e) { - editor.save(); - angularHelper.safeApply($scope, function () { - $scope.model.value = editor.getContent(); - }); - }); - - //when buttons modify content - editor.on('ExecCommand', function (e) { - syncContent(editor); - }); - - // Update model on keypress - editor.on('KeyUp', function (e) { - syncContent(editor); - }); - - // Update model on change, i.e. copy/pasted text, plugins altering content - editor.on('SetContent', function (e) { - if (!e.initial) { - syncContent(editor); - } - }); - - - editor.on('ObjectResized', function (e) { - var qs = "?width=" + e.width + "&height=" + e.height; - var srcAttr = $(e.target).attr("src"); - var path = srcAttr.split("?")[0]; - $(e.target).attr("data-mce-src", path + qs); - - syncContent(editor); - }); - - tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) { - $scope.linkPickerOverlay = { - view: "linkpicker", - currentTarget: currentTarget, - show: true, - submit: function(model) { - tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); - $scope.linkPickerOverlay.show = false; - $scope.linkPickerOverlay = null; - } - }; - }); - - //Create the insert media plugin - tinyMceService.createMediaPicker(editor, $scope, function(currentTarget, userData){ - - $scope.mediaPickerOverlay = { - currentTarget: currentTarget, - onlyImages: true, - showDetails: true, - disableFolderSelect: true, - startNodeId: userData.startMediaId, - view: "mediapicker", - show: true, - submit: function(model) { - tinyMceService.insertMediaInEditor(editor, model.selectedImages[0]); - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - } - }; - - }); - - //Create the embedded plugin - tinyMceService.createInsertEmbeddedMedia(editor, $scope, function() { - - $scope.embedOverlay = { - view: "embed", - show: true, - submit: function(model) { - tinyMceService.insertEmbeddedMediaInEditor(editor, model.embed.preview); - $scope.embedOverlay.show = false; - $scope.embedOverlay = null; - } - }; - - }); - - - //Create the insert macro plugin - tinyMceService.createInsertMacro(editor, $scope, function(dialogData) { - - $scope.macroPickerOverlay = { - view: "macropicker", - dialogData: dialogData, - show: true, - submit: function(model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); - tinyMceService.insertMacroInEditor(editor, macroObject, $scope); - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - } - }; - - }); - }; - - - - - /** Loads in the editor */ - function loadTinyMce() { - - //we need to add a timeout here, to force a redraw so TinyMCE can find - //the elements needed - $timeout(function () { - tinymce.DOM.events.domLoaded = true; - tinymce.init(baseLineConfigObj); - - $scope.isLoading = false; - - }, 200, false); - } - - - - - loadTinyMce(); - - //here we declare a special method which will be called whenever the value has changed from the server - //this is instead of doing a watch on the model.value = faster - $scope.model.onValueChanged = function (newVal, oldVal) { - //update the display val again if it has changed from the server; - tinyMceEditor.setContent(newVal, { format: 'raw' }); - //we need to manually fire this event since it is only ever fired based on loading from the DOM, this - // is required for our plugins listening to this event to execute - tinyMceEditor.fire('LoadContent', null); - }; - - //listen for formSubmitting event (the result is callback used to remove the event subscription) - var unsubscribe = $scope.$on("formSubmitting", function () { - //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer - // we do parse it out on the server side but would be nice to do that on the client side before as well. - $scope.model.value = tinyMceEditor.getContent(); - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom - // element might still be there even after the modal has been hidden. - $scope.$on('$destroy', function () { - unsubscribe(); - }); - }); - }); - - }); - -angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", - function ($scope, $timeout, $log, tinyMceService, stylesheetResource, assetsService) { - var cfg = tinyMceService.defaultPrevalues(); - - if($scope.model.value){ - if(angular.isString($scope.model.value)){ - $scope.model.value = cfg; + function makeBreadcrumbs() { + vm.breadcrumbs = [ + { + 'name': vm.labels.users, + 'path': '/users/users/overview', + 'subView': 'users' + }, + { 'name': vm.user.name } + ]; } - }else{ - $scope.model.value = cfg; - } - - if (!$scope.model.value.stylesheets) { - $scope.model.value.stylesheets = []; - } - if (!$scope.model.value.toolbar) { - $scope.model.value.toolbar = []; - } - if (!$scope.model.value.maxImageSize && $scope.model.value.maxImageSize != 0) { - $scope.model.value.maxImageSize = cfg.maxImageSize; + function setUserDisplayState() { + vm.user.userDisplayState = usersHelper.getUserStateFromValue(vm.user.userState); + } + function formatDatesToLocal(user) { + // get current backoffice user and format dates + userService.getCurrentUser().then(function (currentUser) { + user.formattedLastLogin = getLocalDate(user.lastLoginDate, currentUser.locale, 'LLL'); + user.formattedLastLockoutDate = getLocalDate(user.lastLockoutDate, currentUser.locale, 'LLL'); + user.formattedCreateDate = getLocalDate(user.createDate, currentUser.locale, 'LLL'); + user.formattedUpdateDate = getLocalDate(user.updateDate, currentUser.locale, 'LLL'); + user.formattedLastPasswordChangeDate = getLocalDate(user.lastPasswordChangeDate, currentUser.locale, 'LLL'); + }); + } + init(); } - - tinyMceService.configuration().then(function(config){ - $scope.tinyMceConfig = config; - - // extend commands with properties for font-icon and if it is a custom command - $scope.tinyMceConfig.commands = _.map($scope.tinyMceConfig.commands, function (obj) { - var icon = getFontIcon(obj.frontEndCommand); - return angular.extend(obj, { - fontIcon: icon.name, - isCustom: icon.isCustom + angular.module('umbraco').controller('Umbraco.Editors.Users.UserController', UserEditController); + }()); + (function () { + 'use strict'; + function UserGroupsController($scope, $timeout, $location, userService, userGroupsResource, formHelper, localizationService) { + var vm = this; + vm.userGroups = []; + vm.selection = []; + vm.createUserGroup = createUserGroup; + vm.clickUserGroup = clickUserGroup; + vm.clearSelection = clearSelection; + vm.selectUserGroup = selectUserGroup; + vm.deleteUserGroups = deleteUserGroups; + var currentUser = null; + function onInit() { + vm.loading = true; + userService.getCurrentUser().then(function (user) { + currentUser = user; + // Get usergroups + userGroupsResource.getUserGroups({ onlyCurrentUserGroups: false }).then(function (userGroups) { + // only allow editing and selection if user is member of the group or admin + vm.userGroups = _.map(userGroups, function (ug) { + return { + group: ug, + hasAccess: user.userGroups.indexOf(ug.alias) !== -1 || user.userGroups.indexOf('admin') !== -1 + }; + }); + vm.loading = false; + }); }); - }); - }); - - stylesheetResource.getAll().then(function(stylesheets){ - $scope.stylesheets = stylesheets; - }); - - $scope.selected = function(cmd, alias, lookup){ - if (lookup && angular.isArray(lookup)) { - cmd.selected = lookup.indexOf(alias) >= 0; - return cmd.selected; } - return false; - }; - - $scope.selectCommand = function(command){ - var index = $scope.model.value.toolbar.indexOf(command.frontEndCommand); - - if(command.selected && index === -1){ - $scope.model.value.toolbar.push(command.frontEndCommand); - }else if(index >= 0){ - $scope.model.value.toolbar.splice(index, 1); + function createUserGroup() { + // clear all query params + $location.search({}); + // go to create user group + $location.path('users/users/group/-1').search('create', 'true'); + ; } - }; - - $scope.selectStylesheet = function (css) { - - var index = $scope.model.value.stylesheets.indexOf(css.name); - - if(css.selected && index === -1){ - $scope.model.value.stylesheets.push(css.name); - }else if(index >= 0){ - $scope.model.value.stylesheets.splice(index, 1); + function clickUserGroup(userGroup) { + // only allow editing if user is member of the group or admin + if (currentUser.userGroups.indexOf(userGroup.group.alias) === -1 && currentUser.userGroups.indexOf('admin') === -1) { + return; + } + if (vm.selection.length > 0) { + selectUserGroup(userGroup, vm.selection); + } else { + goToUserGroup(userGroup.group.id); + } } - }; - - // map properties for specific commands - function getFontIcon(alias) { - var icon = { name: alias, isCustom: false }; - - switch (alias) { - case "codemirror": - icon.name = "code"; - icon.isCustom = false; - break; - case "styleselect": - icon.name = "icon-list"; - icon.isCustom = true; - break; - case "umbembeddialog": - icon.name = "icon-tv"; - icon.isCustom = true; - break; - case "umbmediapicker": - icon.name = "icon-picture"; - icon.isCustom = true; - break; - case "umbmacro": - icon.name = "icon-settings-alt"; - icon.isCustom = true; - break; - case "umbmacro": - icon.name = "icon-settings-alt"; - icon.isCustom = true; - break; - default: - icon.name = alias; - icon.isCustom = false; + function selectUserGroup(userGroup, selection, event) { + // Only allow selection if user is member of the group or admin + if (currentUser.userGroups.indexOf(userGroup.group.alias) === -1 && currentUser.userGroups.indexOf('admin') === -1) { + return; + } + // Disallow selection of the admin/translators group, the checkbox is not visible in the UI, but clicking(and thus selecting) is still possible. + // Currently selection can only be used for deleting, and the Controller will also disallow deleting the admin group. + if (userGroup.group.alias === 'admin' || userGroup.group.alias === 'translator') + return; + if (userGroup.selected) { + var index = selection.indexOf(userGroup.group.id); + selection.splice(index, 1); + userGroup.selected = false; + } else { + userGroup.selected = true; + vm.selection.push(userGroup.group.id); + } + if (event) { + event.preventDefault(); + event.stopPropagation(); + } } - - return icon; - } - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - - var commands = _.where($scope.tinyMceConfig.commands, {selected: true}); - $scope.model.value.toolbar = _.pluck(commands, "frontEndCommand"); - - }); - - // when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - // load TinyMCE skin which contains css for font-icons - assetsService.loadCss("lib/tinymce/skins/umbraco/skin.min.css"); - }); -function sliderController($scope, $log, $element, assetsService, angularHelper) { - - //configure some defaults - if (!$scope.model.config.orientation) { - $scope.model.config.orientation = "horizontal"; - } - if (!$scope.model.config.enableRange) { - $scope.model.config.enableRange = false; - } - else { - $scope.model.config.enableRange = $scope.model.config.enableRange === "1" ? true : false; - } - - if (!$scope.model.config.initVal1) { - $scope.model.config.initVal1 = 0; - } - else { - $scope.model.config.initVal1 = parseFloat($scope.model.config.initVal1); - } - if (!$scope.model.config.initVal2) { - $scope.model.config.initVal2 = 0; - } - else { - $scope.model.config.initVal2 = parseFloat($scope.model.config.initVal2); - } - if (!$scope.model.config.minVal) { - $scope.model.config.minVal = 0; - } - else { - $scope.model.config.minVal = parseFloat($scope.model.config.minVal); - } - if (!$scope.model.config.maxVal) { - $scope.model.config.maxVal = 100; - } - else { - $scope.model.config.maxVal = parseFloat($scope.model.config.maxVal); - } - if (!$scope.model.config.step) { - $scope.model.config.step = 1; - } - else { - $scope.model.config.step = parseFloat($scope.model.config.step); - } - - if (!$scope.model.config.handle) { - $scope.model.config.handle = "round"; - } - - if (!$scope.model.config.reversed) { - $scope.model.config.reversed = false; - } - else { - $scope.model.config.reversed = $scope.model.config.reversed === "1" ? true : false; - } - - if (!$scope.model.config.tooltip) { - $scope.model.config.tooltip = "show"; - } - - if (!$scope.model.config.tooltipSplit) { - $scope.model.config.tooltipSplit = false; - } - else { - $scope.model.config.tooltipSplit = $scope.model.config.tooltipSplit === "1" ? true : false; - } - - if ($scope.model.config.tooltipFormat) { - $scope.model.config.formatter = function (value) { - if (angular.isArray(value) && $scope.model.config.enableRange) { - return $scope.model.config.tooltipFormat.replace("{0}", value[0]).replace("{1}", value[1]); - } else { - return $scope.model.config.tooltipFormat.replace("{0}", value); - } - } - } - - if (!$scope.model.config.ticks) { - $scope.model.config.ticks = []; - } - else { - // returns comma-separated string to an array, e.g. [0, 100, 200, 300, 400] - $scope.model.config.ticks = _.map($scope.model.config.ticks.split(','), function (item) { - return parseInt(item.trim()); - }); - } - - if (!$scope.model.config.ticksPositions) { - $scope.model.config.ticksPositions = []; - } - else { - // returns comma-separated string to an array, e.g. [0, 30, 60, 70, 90, 100] - $scope.model.config.ticksPositions = _.map($scope.model.config.ticksPositions.split(','), function (item) { - return parseInt(item.trim()); - }); - console.log($scope.model.config.ticksPositions); - } - - if (!$scope.model.config.ticksLabels) { - $scope.model.config.ticksLabels = []; - } - else { - // returns comma-separated string to an array, e.g. ['$0', '$100', '$200', '$300', '$400'] - $scope.model.config.ticksLabels = _.map($scope.model.config.ticksLabels.split(','), function (item) { - return item.trim(); - }); - } - - if (!$scope.model.config.ticksSnapBounds) { - $scope.model.config.ticksSnapBounds = 0; - } - else { - $scope.model.config.ticksSnapBounds = parseFloat($scope.model.config.ticksSnapBounds); - } - - /** This creates the slider with the model values - it's called on startup and if the model value changes */ - function createSlider() { - - //the value that we'll give the slider - if it's a range, we store our value as a comma separated val but this slider expects an array - var sliderVal = null; - - //configure the model value based on if range is enabled or not - if ($scope.model.config.enableRange == true) { - //If no value saved yet - then use default value - //If it contains a single value - then also create a new array value - if (!$scope.model.value || $scope.model.value.indexOf(",") == -1) { - var i1 = parseFloat($scope.model.config.initVal1); - var i2 = parseFloat($scope.model.config.initVal2); - sliderVal = [ - isNaN(i1) ? $scope.model.config.minVal : (i1 >= $scope.model.config.minVal ? i1 : $scope.model.config.minVal), - isNaN(i2) ? $scope.model.config.maxVal : (i2 > i1 ? (i2 <= $scope.model.config.maxVal ? i2 : $scope.model.config.maxVal) : $scope.model.config.maxVal) - ]; - } - else { - //this will mean it's a delimited value stored in the db, convert it to an array - sliderVal = _.map($scope.model.value.split(','), function (item) { - return parseFloat(item); - }); - } - } - else { - //If no value saved yet - then use default value - if ($scope.model.value) { - sliderVal = parseFloat($scope.model.value); - } - else { - sliderVal = $scope.model.config.initVal1; - } - } - - // Initialise model value if not set - if (!$scope.model.value) { - setModelValueFromSlider(sliderVal); - } - - //initiate slider, add event handler and get the instance reference (stored in data) - var slider = $element.find('.slider-item').bootstrapSlider({ - max: $scope.model.config.maxVal, - min: $scope.model.config.minVal, - orientation: $scope.model.config.orientation, - selection: $scope.model.config.reversed ? "after" : "before", - step: $scope.model.config.step, - precision: $scope.model.config.precision, - tooltip: $scope.model.config.tooltip, - tooltip_split: $scope.model.config.tooltipSplit, - tooltip_position: $scope.model.config.tooltipPosition, - handle: $scope.model.config.handle, - reversed: $scope.model.config.reversed, - ticks: $scope.model.config.ticks, - ticks_positions: $scope.model.config.ticksPositions, - ticks_labels: $scope.model.config.ticksLabels, - ticks_snap_bounds: $scope.model.config.ticksSnapBounds, - formatter: $scope.model.config.formatter, - range: $scope.model.config.enableRange, - //set the slider val - we cannot do this with data- attributes when using ranges - value: sliderVal - }).on('slideStop', function (e) { - var value = e.value; - angularHelper.safeApply($scope, function () { - setModelValueFromSlider(value); - }); - }).data('slider'); - } - - /** Called on start-up when no model value has been applied and on change of the slider via the UI - updates - the model with the currently selected slider value(s) **/ - function setModelValueFromSlider(sliderVal) { - //Get the value from the slider and format it correctly, if it is a range we want a comma delimited value - if ($scope.model.config.enableRange == true) { - $scope.model.value = sliderVal.join(","); - } - else { - $scope.model.value = sliderVal.toString(); - } - } - - //tell the assetsService to load the bootstrap slider - //libs from the plugin folder - assetsService - .loadJs("lib/slider/js/bootstrap-slider.js") - .then(function () { - - createSlider(); - - //here we declare a special method which will be called whenever the value has changed from the server - //this is instead of doing a watch on the model.value = faster - $scope.model.onValueChanged = function (newVal, oldVal) { - if (newVal != oldVal) { - createSlider(); - } - }; - - }); - - //load the separate css for the editor to avoid it blocking our js loading - assetsService.loadCss("lib/slider/bootstrap-slider.css"); - assetsService.loadCss("lib/slider/bootstrap-slider-custom.css"); -} -angular.module("umbraco").controller("Umbraco.PropertyEditors.SliderController", sliderController); -angular.module("umbraco") -.controller("Umbraco.PropertyEditors.TagsController", - function ($rootScope, $scope, $log, assetsService, umbRequestHelper, angularHelper, $timeout, $element) { - - var $typeahead; - - $scope.isLoading = true; - $scope.tagToAdd = ""; - - assetsService.loadJs("lib/typeahead.js/typeahead.bundle.min.js").then(function () { - - $scope.isLoading = false; - - //load current value - - if ($scope.model.value) { - if (!$scope.model.config.storageType || $scope.model.config.storageType !== "Json") { - //it is csv - if (!$scope.model.value) { - $scope.model.value = []; - } - else { - if($scope.model.value.length > 0) { - $scope.model.value = $scope.model.value.split(","); - } - } - } - } - else { - $scope.model.value = []; - } - - // Method required by the valPropertyValidator directive (returns true if the property editor has at least one tag selected) - $scope.validateMandatory = function () { - return { - isValid: !$scope.model.validation.mandatory || ($scope.model.value != null && $scope.model.value.length > 0), - errorMsg: "Value cannot be empty", - errorKey: "required" - }; - } - - //Helper method to add a tag on enter or on typeahead select - function addTag(tagToAdd) { - if (tagToAdd != null && tagToAdd.length > 0) { - if ($scope.model.value.indexOf(tagToAdd) < 0) { - $scope.model.value.push(tagToAdd); - //this is required to re-validate - $scope.propertyForm.tagCount.$setViewValue($scope.model.value.length); - } - } - } - - $scope.addTagOnEnter = function (e) { - var code = e.keyCode || e.which; - if (code == 13) { //Enter keycode - if ($element.find('.tags-' + $scope.model.alias).parent().find(".tt-dropdown-menu .tt-cursor").length === 0) { - //this is required, otherwise the html form will attempt to submit. - e.preventDefault(); - $scope.addTag(); - } - } - }; - - $scope.addTag = function () { - //ensure that we're not pressing the enter key whilst selecting a typeahead value from the drop down - //we need to use jquery because typeahead duplicates the text box - addTag($scope.tagToAdd); - $scope.tagToAdd = ""; - //this clears the value stored in typeahead so it doesn't try to add the text again - // http://issues.umbraco.org/issue/U4-4947 - $typeahead.typeahead('val', ''); - }; - - - - $scope.removeTag = function (tag) { - var i = $scope.model.value.indexOf(tag); - if (i >= 0) { - $scope.model.value.splice(i, 1); - //this is required to re-validate - $scope.propertyForm.tagCount.$setViewValue($scope.model.value.length); - } - }; - - //vice versa - $scope.model.onValueChanged = function (newVal, oldVal) { - //update the display val again if it has changed from the server - $scope.model.value = newVal; - - if (!$scope.model.config.storageType || $scope.model.config.storageType !== "Json") { - //it is csv - if (!$scope.model.value) { - $scope.model.value = []; - } - else { - $scope.model.value = $scope.model.value.split(","); - } - } - }; - - //configure the tags data source - - //helper method to format the data for bloodhound - function dataTransform(list) { - //transform the result to what bloodhound wants - var tagList = _.map(list, function (i) { - return { value: i.text }; - }); - // remove current tags from the list - return $.grep(tagList, function (tag) { - return ($.inArray(tag.value, $scope.model.value) === -1); - }); - } - - // helper method to remove current tags - function removeCurrentTagsFromSuggestions(suggestions) { - return $.grep(suggestions, function (suggestion) { - return ($.inArray(suggestion.value, $scope.model.value) === -1); - }); - } - - var tagsHound = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - dupDetector : function(remoteMatch, localMatch) { - return (remoteMatch["value"] == localMatch["value"]); - }, - //pre-fetch the tags for this category - prefetch: { - url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", [{ tagGroup: $scope.model.config.group }]), - //TTL = 5 minutes - ttl: 300000, - filter: dataTransform - }, - //dynamically get the tags for this category (they may have changed on the server) - remote: { - url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", [{ tagGroup: $scope.model.config.group }]), - filter: dataTransform - } - }); - - tagsHound.initialize(true); - - //configure the type ahead - $timeout(function () { - - $typeahead = $element.find('.tags-' + $scope.model.alias).typeahead( - { - //This causes some strangeness as it duplicates the textbox, best leave off for now. - hint: false, - highlight: true, - cacheKey: new Date(), // Force a cache refresh each time the control is initialized - minLength: 1 - }, { - //see: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#options - // name = the data set name, we'll make this the tag group name - name: $scope.model.config.group, - displayKey: "value", - source: function (query, cb) { - tagsHound.get(query, function (suggestions) { - cb(removeCurrentTagsFromSuggestions(suggestions)); - }); - }, - }).bind("typeahead:selected", function (obj, datum, name) { - angularHelper.safeApply($scope, function () { - addTag(datum["value"]); - $scope.tagToAdd = ""; - // clear the typed text - $typeahead.typeahead('val', ''); - }); - - }).bind("typeahead:autocompleted", function (obj, datum, name) { - angularHelper.safeApply($scope, function () { - addTag(datum["value"]); - $scope.tagToAdd = ""; - }); - - }).bind("typeahead:opened", function (obj) { - //console.log("opened "); - }); - }); - - $scope.$on('$destroy', function () { - tagsHound.clearPrefetchCache(); - tagsHound.clearRemoteCache(); - $element.find('.tags-' + $scope.model.alias).typeahead('destroy'); - delete tagsHound; - }); - - }); - - } -); -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -angular.module('umbraco').controller("Umbraco.PropertyEditors.EmbeddedContentController", - function($rootScope, $scope, $log){ - - $scope.showForm = false; - $scope.fakeData = []; - - $scope.create = function(){ - $scope.showForm = true; - $scope.fakeData = angular.copy($scope.model.config.fields); - }; - - $scope.show = function(){ - $scope.showCode = true; - }; - - $scope.add = function(){ - $scope.showForm = false; - if ( !($scope.model.value instanceof Array)) { - $scope.model.value = []; - } - - $scope.model.value.push(angular.copy($scope.fakeData)); - $scope.fakeData = []; - }; -}); -angular.module('umbraco').controller("Umbraco.PropertyEditors.UrlListController", - function($rootScope, $scope, $filter) { - - function formatDisplayValue() { - if (angular.isArray($scope.model.value)) { - //it's the json value - $scope.renderModel = _.map($scope.model.value, function (item) { - return { - url: item.url, - linkText: item.linkText, - urlTarget: (item.target) ? item.target : "_blank", - icon: (item.icon) ? item.icon : "icon-out" - }; - }); - } - else { - //it's the default csv value - $scope.renderModel = _.map($scope.model.value.split(","), function (item) { - return { - url: item, - linkText: "", - urlTarget: ($scope.config && $scope.config.target) ? $scope.config.target : "_blank", - icon: ($scope.config && $scope.config.icon) ? $scope.config.icon : "icon-out" - }; - }); - } - } - - $scope.getUrl = function(valueUrl) { - if (valueUrl.indexOf("/") >= 0) { - return valueUrl; - } - return "#"; - }; - - formatDisplayValue(); - - //here we declare a special method which will be called whenever the value has changed from the server - //this is instead of doing a watch on the model.value = faster - $scope.model.onValueChanged = function(newVal, oldVal) { - //update the display val again - formatDisplayValue(); - }; - - }); -(function () { - "use strict"; - - function ScriptsCreateController($scope, $location, navigationService, formHelper, codefileResource, localizationService, appState) { - - var vm = this; - var node = $scope.dialogOptions.currentNode; - var localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); - - vm.creatingFolder = false; - vm.folderName = ""; - vm.createFolderError = ""; - vm.fileExtension = ""; - - vm.createFile = createFile; - vm.showCreateFolder = showCreateFolder; - vm.createFolder = createFolder; - - function createFile() { - $location.path("/settings/scripts/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - } - - function showCreateFolder() { - vm.creatingFolder = true; - } - - function createFolder(form) { - - if (formHelper.submitForm({scope: $scope, formCtrl: form, statusMessage: localizeCreateFolder})) { - - codefileResource.createContainer("scripts", node.id, vm.folderName).then(function (saved) { - - navigationService.hideMenu(); - - navigationService.syncTree({ - tree: "scripts", - path: saved.path, - forceReload: true, - activate: true - }); - - formHelper.resetForm({ - scope: $scope + function deleteUserGroups() { + if (vm.selection.length > 0) { + localizationService.localize('defaultdialogs_confirmdelete').then(function (value) { + var confirmResponse = confirm(value); + if (confirmResponse === true) { + userGroupsResource.deleteUserGroups(vm.selection).then(function (data) { + clearSelection(); + onInit(); + formHelper.showNotifications(data); + }, function (error) { + formHelper.showNotifications(error.data); + }); + } }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - vm.createFolderError = err; - - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); + } + } + function clearSelection() { + angular.forEach(vm.userGroups, function (userGroup) { + userGroup.selected = false; + }); + vm.selection = []; + } + function goToUserGroup(userGroupId) { + $location.path('users/users/group/' + userGroupId).search('create', null); + } + onInit(); + } + angular.module('umbraco').controller('Umbraco.Editors.Users.GroupsController', UserGroupsController); + }()); + (function () { + 'use strict'; + function UsersController($scope, $timeout, $location, usersResource, userGroupsResource, userService, localizationService, contentEditingHelper, usersHelper, formHelper, notificationsService, dateHelper) { + var vm = this; + var localizeSaving = localizationService.localize('general_saving'); + vm.page = {}; + vm.users = []; + vm.userGroups = []; + vm.userStates = []; + vm.selection = []; + vm.newUser = {}; + vm.usersOptions = {}; + vm.userSortData = [ + { + label: 'Name (A-Z)', + key: 'Name', + direction: 'Ascending' + }, + { + label: 'Name (Z-A)', + key: 'Name', + direction: 'Descending' + }, + { + label: 'Newest', + key: 'CreateDate', + direction: 'Descending' + }, + { + label: 'Oldest', + key: 'CreateDate', + direction: 'Ascending' + }, + { + label: 'Last login', + key: 'LastLoginDate', + direction: 'Descending' + } + ]; + angular.forEach(vm.userSortData, function (userSortData) { + var key = 'user_sort' + userSortData.key + userSortData.direction; + localizationService.localize(key).then(function (value) { + var reg = /^\[[\S\s]*]$/g; + var result = reg.test(value); + if (result === false) { + // Only translate if key exists + userSortData.label = value; + } + }); + }); + vm.userStatesFilter = []; + vm.newUser.userGroups = []; + vm.usersViewState = 'overview'; + vm.selectedBulkUserGroups = []; + vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail; + vm.allowDisableUser = true; + vm.allowEnableUser = true; + vm.allowUnlockUser = true; + vm.allowSetUserGroup = true; + vm.layouts = [ + { + 'icon': 'icon-thumbnails-small', + 'path': '1', + 'selected': true + }, + { + 'icon': 'icon-list', + 'path': '2', + 'selected': true + } + ]; + vm.activeLayout = { + 'icon': 'icon-thumbnails-small', + 'path': '1', + 'selected': true + }; + //don't show the invite button if no email is configured + if (Umbraco.Sys.ServerVariables.umbracoSettings.showUserInvite) { + vm.defaultButton = { + labelKey: 'user_inviteUser', + handler: function () { + vm.setUsersViewState('inviteUser'); + } + }; + vm.subButtons = [{ + labelKey: 'user_createUser', + handler: function () { + vm.setUsersViewState('createUser'); } + }]; + } else { + vm.defaultButton = { + labelKey: 'user_createUser', + handler: function () { + vm.setUsersViewState('createUser'); } + }; + } + vm.toggleFilter = toggleFilter; + vm.setUsersViewState = setUsersViewState; + vm.selectLayout = selectLayout; + vm.selectUser = selectUser; + vm.clearSelection = clearSelection; + vm.clickUser = clickUser; + vm.disableUsers = disableUsers; + vm.enableUsers = enableUsers; + vm.unlockUsers = unlockUsers; + vm.openBulkUserGroupPicker = openBulkUserGroupPicker; + vm.openUserGroupPicker = openUserGroupPicker; + vm.removeSelectedUserGroup = removeSelectedUserGroup; + vm.selectAll = selectAll; + vm.areAllSelected = areAllSelected; + vm.searchUsers = searchUsers; + vm.getFilterName = getFilterName; + vm.setUserStatesFilter = setUserStatesFilter; + vm.setUserGroupFilter = setUserGroupFilter; + vm.setOrderByFilter = setOrderByFilter; + vm.changePageNumber = changePageNumber; + vm.createUser = createUser; + vm.inviteUser = inviteUser; + vm.getSortLabel = getSortLabel; + vm.toggleNewUserPassword = toggleNewUserPassword; + vm.copySuccess = copySuccess; + vm.copyError = copyError; + vm.goToUser = goToUser; + function init() { + vm.usersOptions.orderBy = 'Name'; + vm.usersOptions.orderDirection = 'Ascending'; + // Get users + getUsers(); + // Get user groups + userGroupsResource.getUserGroups({ onlyCurrentUserGroups: false }).then(function (userGroups) { + vm.userGroups = userGroups; }); } - - } - - } - - angular.module("umbraco").controller("Umbraco.Editors.Scripts.CreateController", ScriptsCreateController); -})(); - -/** - * @ngdoc controller - * @name Umbraco.Editors.Scripts.DeleteController - * @function - * - * @description - * The controller for deleting scripts - */ -function ScriptsDeleteController($scope, codefileResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - - codefileResource.deleteByPath('scripts', $scope.currentNode.id) - .then(function() { - $scope.currentNode.loading = false; - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Scripts.DeleteController", ScriptsDeleteController); - -(function () { - "use strict"; - - function ScriptsEditController($scope, $routeParams, $timeout, appState, editorState, navigationService, assetsService, codefileResource, contentEditingHelper, notificationsService, localizationService, templateHelper, angularHelper) { - - var vm = this; - var currentPosition = null; - var localizeSaving = localizationService.localize("general_saving"); - - vm.page = {}; - vm.page.loading = true; - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; - vm.page.saveButtonState = "init"; - - //Used to toggle the keyboard shortcut modal - //From a custom keybinding in ace editor - that conflicts with our own to show the dialog - vm.showKeyboardShortcut = false; - - //Keyboard shortcuts for help dialog - vm.page.keyboardShortcutsOverview = []; - vm.page.keyboardShortcutsOverview.push(templateHelper.getGeneralShortcuts()); - vm.page.keyboardShortcutsOverview.push(templateHelper.getEditorShortcuts()); - - - vm.script = {}; - - // bind functions to view model - vm.save = save; - - /* Function bound to view model */ - - function save() { - - vm.page.saveButtonState = "busy"; - - vm.script.content = vm.editor.getValue(); - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: localizeSaving, - saveMethod: codefileResource.save, - scope: $scope, - content: vm.script, - // We do not redirect on failure for scripts - this is because it is not possible to actually save the script - // when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - rebindCallback: function (orignal, saved) {} - }).then(function (saved) { - - localizationService.localizeMany(["speechBubbles_fileSavedHeader", "speechBubbles_fileSavedText"]).then(function(data){ - var header = data[0]; - var message = data[1]; - notificationsService.success(header, message); - }); - - //check if the name changed, if so we need to redirect - if (vm.script.id !== saved.id) { - contentEditingHelper.redirectToRenamedContent(saved.id); + function getSortLabel(sortKey, sortDirection) { + var found = _.find(vm.userSortData, function (i) { + return i.key === sortKey && i.direction === sortDirection; + }); + return found ? found.label : sortKey; + } + function toggleFilter(type) { + // hack: on-outside-click prevents us from closing the dropdown when clicking on another link + // so I had to do this manually + switch (type) { + case 'state': + vm.page.showStatusFilter = !vm.page.showStatusFilter; + vm.page.showGroupFilter = false; + vm.page.showOrderByFilter = false; + break; + case 'group': + vm.page.showGroupFilter = !vm.page.showGroupFilter; + vm.page.showStatusFilter = false; + vm.page.showOrderByFilter = false; + break; + case 'orderBy': + vm.page.showOrderByFilter = !vm.page.showOrderByFilter; + vm.page.showStatusFilter = false; + vm.page.showGroupFilter = false; + break; } - else { - vm.page.saveButtonState = "success"; - vm.script = saved; - - //sync state - editorState.set(vm.script); - - // sync tree - navigationService.syncTree({ tree: "scripts", path: vm.script.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); + } + function setUsersViewState(state) { + if (state === 'createUser') { + clearAddUserForm(); } - - }, function (err) { - - vm.page.saveButtonState = "error"; - - localizationService.localizeMany(["speechBubbles_validationFailedHeader", "speechBubbles_validationFailedMessage"]).then(function(data){ - var header = data[0]; - var message = data[1]; - notificationsService.error(header, message); - }); - - }); - - - } - - /* Local functions */ - - function init() { - - //we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css"); - - if ($routeParams.create) { - codefileResource.getScaffold("scripts", $routeParams.id).then(function (script) { - ready(script, false); + vm.usersViewState = state; + } + function selectLayout(selectedLayout) { + angular.forEach(vm.layouts, function (layout) { + layout.active = false; }); - } else { - codefileResource.getByPath('scripts', $routeParams.id).then(function (script) { - ready(script, true); + selectedLayout.active = true; + vm.activeLayout = selectedLayout; + } + function selectUser(user, selection, event) { + // prevent the current user to be selected + if (!user.isCurrentUser) { + if (user.selected) { + var index = selection.indexOf(user.id); + selection.splice(index, 1); + user.selected = false; + } else { + user.selected = true; + vm.selection.push(user.id); + } + setBulkActions(vm.users); + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + function clearSelection() { + angular.forEach(vm.users, function (user) { + user.selected = false; }); + vm.selection = []; } - - } - - function ready(script, syncTree) { - - vm.page.loading = false; - - vm.script = script; - - //sync state - editorState.set(vm.script); - - if (syncTree) { - navigationService.syncTree({ tree: "scripts", path: vm.script.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; + function clickUser(user) { + if (vm.selection.length > 0) { + selectUser(user, vm.selection); + } else { + goToUser(user.id); + } + } + function disableUsers() { + vm.disableUserButtonState = 'busy'; + usersResource.disableUsers(vm.selection).then(function (data) { + // update userState + angular.forEach(vm.selection, function (userId) { + var user = getUserFromArrayById(userId, vm.users); + if (user) { + user.userState = 1; + } + }); + // show the correct badges + setUserDisplayState(vm.users); + formHelper.showNotifications(data); + vm.disableUserButtonState = 'init'; + clearSelection(); + }, function (error) { + vm.disableUserButtonState = 'error'; + formHelper.showNotifications(error.data); }); } - - vm.aceOption = { - mode: "javascript", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px', - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: false - }, - onLoad: function(_editor) { - - vm.editor = _editor; - - //Update the auto-complete method to use ctrl+alt+space - _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); - - //Unassigns the keybinding (That was previously auto-complete) - //As conflicts with our own tree search shortcut - _editor.commands.bindKey("ctrl-space", null); - - //TODO: Move all these keybinding config out into some helper/service - _editor.commands.addCommands([ - //Disable (alt+shift+K) - //Conflicts with our own show shortcuts dialog - this overrides it - { - name: 'unSelectOrFindPrevious', - bindKey: 'Alt-Shift-K', - exec: function() { - //Toggle the show keyboard shortcuts overlay - $scope.$apply(function(){ - vm.showKeyboardShortcut = !vm.showKeyboardShortcut; - }); - }, - readOnly: true - }, - ]); - - // initial cursor placement - // Keep cursor in name field if we are create a new script - // else set the cursor at the bottom of the code editor - if(!$routeParams.create) { - $timeout(function(){ - vm.editor.navigateFileEnd(); - vm.editor.focus(); + function enableUsers() { + vm.enableUserButtonState = 'busy'; + usersResource.enableUsers(vm.selection).then(function (data) { + // update userState + angular.forEach(vm.selection, function (userId) { + var user = getUserFromArrayById(userId, vm.users); + if (user) { + user.userState = 0; + } + }); + // show the correct badges + setUserDisplayState(vm.users); + // show notification + formHelper.showNotifications(data); + vm.enableUserButtonState = 'init'; + clearSelection(); + }, function (error) { + vm.enableUserButtonState = 'error'; + formHelper.showNotifications(error.data); + }); + } + function unlockUsers() { + vm.unlockUserButtonState = 'busy'; + usersResource.unlockUsers(vm.selection).then(function (data) { + // update userState + angular.forEach(vm.selection, function (userId) { + var user = getUserFromArrayById(userId, vm.users); + if (user) { + user.userState = 0; + } + }); + // show the correct badges + setUserDisplayState(vm.users); + // show notification + formHelper.showNotifications(data); + vm.unlockUserButtonState = 'init'; + clearSelection(); + }, function (error) { + vm.unlockUserButtonState = 'error'; + formHelper.showNotifications(error.data); + }); + } + function getUserFromArrayById(userId, users) { + return _.find(users, function (u) { + return u.id === userId; + }); + } + function openBulkUserGroupPicker(event) { + var firstSelectedUser = getUserFromArrayById(vm.selection[0], vm.users); + vm.selectedBulkUserGroups = _.clone(firstSelectedUser.userGroups); + vm.userGroupPicker = { + title: localizationService.localize('user_selectUserGroups'), + view: 'usergrouppicker', + selection: vm.selectedBulkUserGroups, + closeButtonLabel: localizationService.localize('general_cancel'), + show: true, + submit: function (model) { + usersResource.setUserGroupsOnUsers(model.selection, vm.selection).then(function (data) { + // sorting to ensure they show up in right order when updating the UI + vm.selectedBulkUserGroups.sort(function (a, b) { + return a.alias > b.alias ? 1 : a.alias < b.alias ? -1 : 0; + }); + // apply changes to UI + _.each(vm.selection, function (userId) { + var user = getUserFromArrayById(userId, vm.users); + user.userGroups = vm.selectedBulkUserGroups; + }); + vm.selectedBulkUserGroups = []; + vm.userGroupPicker.show = false; + vm.userGroupPicker = null; + formHelper.showNotifications(data); + clearSelection(); + }, function (error) { + formHelper.showNotifications(error.data); }); + }, + close: function (oldModel) { + vm.selectedBulkUserGroups = []; + vm.userGroupPicker.show = false; + vm.userGroupPicker = null; } - - vm.editor.on("change", changeAceEditor); - - } + }; } - - function changeAceEditor() { - setFormState("dirty"); + function openUserGroupPicker(event) { + vm.userGroupPicker = { + title: localizationService.localize('user_selectUserGroups'), + view: 'usergrouppicker', + selection: vm.newUser.userGroups, + closeButtonLabel: localizationService.localize('general_cancel'), + show: true, + submit: function (model) { + // apply changes + if (model.selection) { + vm.newUser.userGroups = model.selection; + } + vm.userGroupPicker.show = false; + vm.userGroupPicker = null; + }, + close: function (oldModel) { + // rollback on close + if (oldModel.selection) { + vm.newUser.userGroups = oldModel.selection; + } + vm.userGroupPicker.show = false; + vm.userGroupPicker = null; + } + }; } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if(state === "dirty") { - currentForm.$setDirty(); - } else if(state === "pristine") { - currentForm.$setPristine(); - } + function removeSelectedUserGroup(index, selection) { + selection.splice(index, 1); } - - - } - - init(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.Scripts.EditController", ScriptsEditController); -})(); -(function () { - "use strict"; - - function TemplatesEditController($scope, $routeParams, $timeout, templateResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, treeService, contentEditingHelper, localizationService, angularHelper, templateHelper) { - - var vm = this; - var oldMasterTemplateAlias = null; - var localizeSaving = localizationService.localize("general_saving"); - - vm.page = {}; - vm.page.loading = true; - vm.templates = []; - - //menu - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; - - //Used to toggle the keyboard shortcut modal - //From a custom keybinding in ace editor - that conflicts with our own to show the dialog - vm.showKeyboardShortcut = false; - - //Keyboard shortcuts for help dialog - vm.page.keyboardShortcutsOverview = []; - vm.page.keyboardShortcutsOverview.push(templateHelper.getGeneralShortcuts()); - vm.page.keyboardShortcutsOverview.push(templateHelper.getEditorShortcuts()); - vm.page.keyboardShortcutsOverview.push(templateHelper.getTemplateEditorShortcuts()); - - - vm.save = function () { - vm.page.saveButtonState = "busy"; - - vm.template.content = vm.editor.getValue(); - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: localizeSaving, - saveMethod: templateResource.save, - scope: $scope, - content: vm.template, - //We do not redirect on failure for templates - this is because it is not possible to actually save the template - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - rebindCallback: function (orignal, saved) {} - }).then(function (saved) { - - localizationService.localizeMany(["speechBubbles_templateSavedHeader", "speechBubbles_templateSavedText"]).then(function(data){ - var header = data[0]; - var message = data[1]; - notificationsService.success(header, message); - }); - - - vm.page.saveButtonState = "success"; - vm.template = saved; - - //sync state - editorState.set(vm.template); - - // sync tree - // if master template alias has changed move the node to it's new location - if(oldMasterTemplateAlias !== vm.template.masterTemplateAlias) { - - // When creating a new template the id is -1. Make sure We don't remove the root node. - if (vm.page.menu.currentNode.id !== "-1") { - // move node to new location in tree - //first we need to remove the node that we're working on - treeService.removeNode(vm.page.menu.currentNode); - } - - // update stored alias to the new one so the node won't move again unless the alias is changed again - oldMasterTemplateAlias = vm.template.masterTemplateAlias; - - navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true, activate: true }).then(function (args) { - vm.page.menu.currentNode = args.node; + function selectAll() { + if (areAllSelected()) { + vm.selection = []; + angular.forEach(vm.users, function (user) { + user.selected = false; }); - } else { - - // normal tree sync - navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; + // clear selection so we don't add the same user twice + vm.selection = []; + // select all users + angular.forEach(vm.users, function (user) { + // prevent the current user to be selected + if (!user.isCurrentUser) { + user.selected = true; + vm.selection.push(user.id); + } }); - } - - // clear $dirty state on form - setFormState("pristine"); - - - }, function (err) { - - vm.page.saveButtonState = "error"; - - localizationService.localizeMany(["speechBubbles_validationFailedHeader", "speechBubbles_validationFailedMessage"]).then(function(data){ - var header = data[0]; - var message = data[1]; - notificationsService.error(header, message); - }); - - }); - - }; - - vm.init = function () { - - //we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css"); - - //load templates - used in the master template picker - templateResource.getAll() - .then(function(templates) { - vm.templates = templates; + } + function areAllSelected() { + // we need to check if the current user is part of the selection and + // subtract the user from the total selection to find out if all users are selected + var includesCurrentUser = vm.users.some(function (user) { + return user.isCurrentUser === true; }); - - if($routeParams.create){ - - templateResource.getScaffold(($routeParams.id)).then(function (template) { - vm.ready(template); - }); - - }else{ - - templateResource.getById($routeParams.id).then(function(template){ - vm.ready(template); - }); - - } - - }; - - - vm.ready = function(template){ - vm.page.loading = false; - vm.template = template; - - //sync state - editorState.set(vm.template); - navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - - // save state of master template to use for comparison when syncing the tree on save - oldMasterTemplateAlias = angular.copy(template.masterTemplateAlias); - - // ace configuration - vm.aceOption = { - mode: "razor", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px', - enableSnippets: false, //The Razor mode snippets are awful (Need a way to override these) - enableBasicAutocompletion: true, - enableLiveAutocompletion: false - }, - onLoad: function(_editor) { - vm.editor = _editor; - - //Update the auto-complete method to use ctrl+alt+space - _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); - - //Unassigns the keybinding (That was previously auto-complete) - //As conflicts with our own tree search shortcut - _editor.commands.bindKey("ctrl-space", null); - - // Assign new keybinding - _editor.commands.addCommands([ - //Disable (alt+shift+K) - //Conflicts with our own show shortcuts dialog - this overrides it - { - name: 'unSelectOrFindPrevious', - bindKey: 'Alt-Shift-K', - exec: function() { - //Toggle the show keyboard shortcuts overlay - $scope.$apply(function(){ - vm.showKeyboardShortcut = !vm.showKeyboardShortcut; - }); - - }, - readOnly: true - }, - { - name: 'insertUmbracoValue', - bindKey: 'Alt-Shift-V', - exec: function() { - $scope.$apply(function(){ - openPageFieldOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertPartialView', - bindKey: 'Alt-Shift-P', - exec: function() { - $scope.$apply(function(){ - openPartialOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertDictionary', - bindKey: 'Alt-Shift-D', - exec: function() { - $scope.$apply(function(){ - openDictionaryItemOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertUmbracoMacro', - bindKey: 'Alt-Shift-M', - exec: function() { - $scope.$apply(function(){ - openMacroOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertQuery', - bindKey: 'Alt-Shift-Q', - exec: function() { - $scope.$apply(function(){ - openQueryBuilderOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertSection', - bindKey: 'Alt-Shift-S', - exec: function() { - $scope.$apply(function(){ - openSectionsOverlay(); - }); - }, - readOnly: true - }, - { - name: 'chooseMasterTemplate', - bindKey: 'Alt-Shift-T', - exec: function() { - $scope.$apply(function(){ - openMasterTemplateOverlay(); - }); - }, - readOnly: true - }, - - ]); - - // initial cursor placement - // Keep cursor in name field if we are create a new template - // else set the cursor at the bottom of the code editor - if(!$routeParams.create) { - $timeout(function(){ - vm.editor.navigateFileEnd(); - vm.editor.focus(); - persistCurrentLocation(); - }); + if (includesCurrentUser) { + if (vm.selection.length === vm.users.length - 1) { + return true; } - - //change on blur, focus - vm.editor.on("blur", persistCurrentLocation); - vm.editor.on("focus", persistCurrentLocation); - vm.editor.on("change", changeAceEditor); - } - } - - }; - - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openQueryBuilderOverlay = openQueryBuilderOverlay; - vm.openMacroOverlay = openMacroOverlay; - vm.openInsertOverlay = openInsertOverlay; - vm.openSectionsOverlay = openSectionsOverlay; - vm.openPartialOverlay = openPartialOverlay; - vm.openMasterTemplateOverlay = openMasterTemplateOverlay; - vm.selectMasterTemplate = selectMasterTemplate; - vm.getMasterTemplateName = getMasterTemplateName; - vm.removeMasterTemplate = removeMasterTemplate; - - function openInsertOverlay() { - - vm.insertOverlay = { - view: "insert", - allowedTypes: { - macro: true, - dictionary: true, - partial: true, - umbracoField: true - }, - hideSubmitButton: true, - show: true, - submit: function(model) { - - switch(model.insert.type) { - - case "macro": - var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); - insert(macroObject.syntax); - break; - - case "dictionary": - var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); - insert(code); - break; - - case "partial": - var code = templateHelper.getInsertPartialSnippet(model.insert.node.parentId, model.insert.node.name); - insert(code); - break; - - case "umbracoField": - insert(model.insert.umbracoField); - break; + } else { + if (vm.selection.length === vm.users.length) { + return true; } - - vm.insertOverlay.show = false; - vm.insertOverlay = null; - - }, - close: function(oldModel) { - // close the dialog - vm.insertOverlay.show = false; - vm.insertOverlay = null; - // focus editor - vm.editor.focus(); } - }; - - } - - - function openMacroOverlay() { - - vm.macroPickerOverlay = { - view: "macropicker", - dialogData: {}, - show: true, - title: localizationService.localize("template_insertMacro"), - submit: function (model) { - - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); - insert(macroObject.syntax); - - vm.macroPickerOverlay.show = false; - vm.macroPickerOverlay = null; - - }, - close: function(oldModel) { - // close the dialog - vm.macroPickerOverlay.show = false; - vm.macroPickerOverlay = null; - // focus editor - vm.editor.focus(); + } + var search = _.debounce(function () { + $scope.$apply(function () { + getUsers(); + }); + }, 500); + function searchUsers() { + search(); + } + function getFilterName(array) { + var name = 'All'; + var found = false; + angular.forEach(array, function (item) { + if (item.selected) { + if (!found) { + name = item.name; + found = true; + } else { + name = name + ', ' + item.name; + } + } + }); + return name; + } + function setUserStatesFilter(userState) { + if (!vm.usersOptions.userStates) { + vm.usersOptions.userStates = []; } - }; - } - - - function openPageFieldOverlay() { - vm.pageFieldOverlay = { - submitButtonLabel: "Insert", - closeButtonlabel: "Cancel", - view: "insertfield", - show: true, - title: localizationService.localize("template_insertPageField"), - submit: function (model) { - insert(model.umbracoField); - vm.pageFieldOverlay.show = false; - vm.pageFieldOverlay = null; - }, - close: function (model) { - // close the dialog - vm.pageFieldOverlay.show = false; - vm.pageFieldOverlay = null; - // focus editor - vm.editor.focus(); + //If the selection is "ALL" then we need to unselect everything else since this is an 'odd' filter + if (userState.key === 'All') { + angular.forEach(vm.userStatesFilter, function (i) { + i.selected = false; + }); + //we can't unselect All + userState.selected = true; + //reset the selection passed to the server + vm.usersOptions.userStates = []; + } else { + angular.forEach(vm.userStatesFilter, function (i) { + if (i.key === 'All') { + i.selected = false; + } + }); + var indexOfAll = vm.usersOptions.userStates.indexOf('All'); + if (indexOfAll >= 0) { + vm.usersOptions.userStates.splice(indexOfAll, 1); + } } - }; - } - - - function openDictionaryItemOverlay() { - vm.dictionaryItemOverlay = { - view: "treepicker", - section: "settings", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - show: true, - title: localizationService.localize("template_insertDictionaryItem"), - emptyStateMessage: localizationService.localize("emptyStates_emptyDictionaryTree"), - select: function(node){ - var code = templateHelper.getInsertDictionarySnippet(node.name); - insert(code); - - vm.dictionaryItemOverlay.show = false; - vm.dictionaryItemOverlay = null; - }, - close: function (model) { - // close dialog - vm.dictionaryItemOverlay.show = false; - vm.dictionaryItemOverlay = null; - // focus editor - vm.editor.focus(); + if (userState.selected) { + vm.usersOptions.userStates.push(userState.key); + } else { + var index = vm.usersOptions.userStates.indexOf(userState.key); + vm.usersOptions.userStates.splice(index, 1); } - }; - } - - function openPartialOverlay() { - vm.partialItemOverlay = { - view: "treepicker", - section: "settings", - treeAlias: "partialViews", - entityType: "partialView", - multiPicker: false, - show: true, - title: localizationService.localize("template_insertPartialView"), - filter: function(i) { - if(i.name.indexOf(".cshtml") === -1 && i.name.indexOf(".vbhtml") === -1) { - return true; - } - }, - filterCssClass: "not-allowed", - select: function(node){ - - var code = templateHelper.getInsertPartialSnippet(node.parentId, node.name); - insert(code); - - vm.partialItemOverlay.show = false; - vm.partialItemOverlay = null; - }, - close: function (model) { - // close dialog - vm.partialItemOverlay.show = false; - vm.partialItemOverlay = null; - // focus editor - vm.editor.focus(); + getUsers(); + } + function setUserGroupFilter(userGroup) { + if (!vm.usersOptions.userGroups) { + vm.usersOptions.userGroups = []; } - }; - } - - function openQueryBuilderOverlay() { - vm.queryBuilderOverlay = { - view: "querybuilder", - show: true, - title: localizationService.localize("template_queryBuilder"), - submit: function (model) { - - var code = templateHelper.getQuerySnippet(model.result.queryExpression); - insert(code); - - vm.queryBuilderOverlay.show = false; - vm.queryBuilderOverlay = null; - }, - - close: function (model) { - // close dialog - vm.queryBuilderOverlay.show = false; - vm.queryBuilderOverlay = null; - // focus editor - vm.editor.focus(); + if (userGroup.selected) { + vm.usersOptions.userGroups.push(userGroup.alias); + } else { + var index = vm.usersOptions.userGroups.indexOf(userGroup.alias); + vm.usersOptions.userGroups.splice(index, 1); } - }; - } - - - function openSectionsOverlay() { - - vm.sectionsOverlay = { - view: "templatesections", - isMaster: vm.template.isMasterTemplate, - submitButtonLabel: "Insert", - show: true, - submit: function(model) { - - if (model.insertType === 'renderBody') { - var code = templateHelper.getRenderBodySnippet(); - insert(code); + getUsers(); + } + function setOrderByFilter(value, direction) { + vm.usersOptions.orderBy = value; + vm.usersOptions.orderDirection = direction; + getUsers(); + } + function changePageNumber(pageNumber) { + vm.usersOptions.pageNumber = pageNumber; + getUsers(); + } + function createUser(addUserForm) { + if (formHelper.submitForm({ + formCtrl: addUserForm, + scope: $scope, + statusMessage: 'Saving...' + })) { + vm.newUser.id = -1; + vm.newUser.parentId = -1; + vm.page.createButtonState = 'busy'; + usersResource.createUser(vm.newUser).then(function (saved) { + vm.page.createButtonState = 'success'; + vm.newUser = saved; + setUsersViewState('createUserSuccess'); + getUsers(); + }, function (err) { + formHelper.handleError(err); + vm.page.createButtonState = 'error'; + }); + } + } + function inviteUser(addUserForm) { + if (formHelper.submitForm({ + formCtrl: addUserForm, + scope: $scope, + statusMessage: 'Saving...' + })) { + vm.newUser.id = -1; + vm.newUser.parentId = -1; + vm.page.createButtonState = 'busy'; + usersResource.inviteUser(vm.newUser).then(function (saved) { + //success + vm.page.createButtonState = 'success'; + vm.newUser = saved; + setUsersViewState('inviteUserSuccess'); + getUsers(); + }, function (err) { + //error + formHelper.handleError(err); + vm.page.createButtonState = 'error'; + }); + } + } + function toggleNewUserPassword() { + vm.newUser.showPassword = !vm.newUser.showPassword; + } + // copy to clip board success + function copySuccess() { + vm.page.copyPasswordButtonState = 'success'; + } + // copy to clip board error + function copyError() { + vm.page.copyPasswordButtonState = 'error'; + } + function goToUser(userId) { + $location.path('users/users/user/' + userId); + } + // helpers + function getUsers() { + vm.loading = true; + // Get users + usersResource.getPagedResults(vm.usersOptions).then(function (data) { + vm.users = data.items; + vm.usersOptions.pageNumber = data.pageNumber; + vm.usersOptions.pageSize = data.pageSize; + vm.usersOptions.totalItems = data.totalItems; + vm.usersOptions.totalPages = data.totalPages; + formatDates(vm.users); + setUserDisplayState(vm.users); + vm.userStatesFilter = usersHelper.getUserStatesFilter(data.userStates); + vm.loading = false; + }, function (error) { + vm.loading = false; + }); + } + function setUserDisplayState(users) { + angular.forEach(users, function (user) { + user.userDisplayState = usersHelper.getUserStateFromValue(user.userState); + }); + } + function formatDates(users) { + angular.forEach(users, function (user) { + if (user.lastLoginDate) { + var dateVal; + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + var localOffset = new Date().getTimezoneOffset(); + var serverTimeNeedsOffsetting = -serverOffset !== localOffset; + if (serverTimeNeedsOffsetting) { + dateVal = dateHelper.convertToLocalMomentTime(user.lastLoginDate, serverOffset); + } else { + dateVal = moment(user.lastLoginDate, 'YYYY-MM-DD HH:mm:ss'); + } + // get current backoffice user and format date + userService.getCurrentUser().then(function (currentUser) { + user.formattedLastLogin = dateVal.locale(currentUser.locale).format('LLL'); + }); } - - if (model.insertType === 'renderSection') { - var code = templateHelper.getRenderSectionSnippet(model.renderSectionName, model.mandatoryRenderSection); - insert(code); + }); + } + function setBulkActions(users) { + // reset all states + vm.allowDisableUser = true; + vm.allowEnableUser = true; + vm.allowUnlockUser = true; + vm.allowSetUserGroup = true; + var firstSelectedUserGroups; + angular.forEach(users, function (user) { + if (!user.selected) { + return; } - - if (model.insertType === 'addSection') { - var code = templateHelper.getAddSectionSnippet(model.sectionName); - wrap(code); + // if the current user is selected prevent any bulk actions with the user included + if (user.isCurrentUser) { + vm.allowDisableUser = false; + vm.allowEnableUser = false; + vm.allowUnlockUser = false; + vm.allowSetUserGroup = false; + return; } - - vm.sectionsOverlay.show = false; - vm.sectionsOverlay = null; - - }, - close: function(model) { - // close dialog - vm.sectionsOverlay.show = false; - vm.sectionsOverlay = null; - // focus editor - vm.editor.focus(); - } - } - } - - function openMasterTemplateOverlay() { - - // make collection of available master templates - var availableMasterTemplates = []; - - // filter out the current template and the selected master template - angular.forEach(vm.templates, function(template){ - if(template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { - availableMasterTemplates.push(template); - } - }); - - vm.masterTemplateOverlay = { - view: "itempicker", - title: localizationService.localize("template_mastertemplate"), - availableItems: availableMasterTemplates, - show: true, - submit: function(model) { - - var template = model.selectedItem; - - if (template && template.alias) { - vm.template.masterTemplateAlias = template.alias; - setLayout(template.alias + ".cshtml"); - } else { - vm.template.masterTemplateAlias = null; - setLayout(null); + if (user.userDisplayState && user.userDisplayState.key === 'Disabled') { + vm.allowDisableUser = false; } - - vm.masterTemplateOverlay.show = false; - vm.masterTemplateOverlay = null; - }, - close: function(oldModel) { - // close dialog - vm.masterTemplateOverlay.show = false; - vm.masterTemplateOverlay = null; - // focus editor - vm.editor.focus(); - } - }; - - } - - function selectMasterTemplate(template) { - - if (template && template.alias) { - vm.template.masterTemplateAlias = template.alias; - setLayout(template.alias + ".cshtml"); - } else { - vm.template.masterTemplateAlias = null; - setLayout(null); - } - - } - - function getMasterTemplateName(masterTemplateAlias, templates) { - if(masterTemplateAlias) { - var templateName = ""; - angular.forEach(templates, function(template){ - if(template.alias === masterTemplateAlias) { - templateName = template.name; + if (user.userDisplayState && user.userDisplayState.key === 'Active') { + vm.allowEnableUser = false; + } + if (user.userDisplayState && user.userDisplayState.key === 'Invited') { + vm.allowEnableUser = false; + } + if (user.userDisplayState && user.userDisplayState.key === 'LockedOut') { + vm.allowEnableUser = false; + } + if (user.userDisplayState && user.userDisplayState.key !== 'LockedOut') { + vm.allowUnlockUser = false; + } + // store the user group aliases of the first selected user + if (!firstSelectedUserGroups) { + firstSelectedUserGroups = user.userGroups.map(function (ug) { + return ug.alias; + }); + vm.allowSetUserGroup = true; + } else if (vm.allowSetUserGroup === true) { + // for 2nd+ selected user, compare the user group aliases to determine if we should allow bulk editing. + // we don't allow bulk editing of users not currently having the same assigned user groups, as we can't + // really support that in the user group picker. + var userGroups = user.userGroups.map(function (ug) { + return ug.alias; + }); + if (_.difference(firstSelectedUserGroups, userGroups).length > 0) { + vm.allowSetUserGroup = false; + } } }); - return templateName; - } - } - - function removeMasterTemplate() { - - vm.template.masterTemplateAlias = null; - - // call set layout with no paramters to set layout to null - setLayout(); - - } - - function setLayout(templatePath){ - - var templateCode = vm.editor.getValue(); - var newValue = templatePath; - var layoutDefRegex = new RegExp("(@{[\\s\\S]*?Layout\\s*?=\\s*?)(\"[^\"]*?\"|null)(;[\\s\\S]*?})", "gi"); - - if (newValue !== undefined && newValue !== "") { - if (layoutDefRegex.test(templateCode)) { - // Declaration exists, so just update it - templateCode = templateCode.replace(layoutDefRegex, "$1\"" + newValue + "\"$3"); - } else { - // Declaration doesn't exist, so prepend to start of doc - //TODO: Maybe insert at the cursor position, rather than just at the top of the doc? - templateCode = "@{\n\tLayout = \"" + newValue + "\";\n}\n" + templateCode; - } - } else { - if (layoutDefRegex.test(templateCode)) { - // Declaration exists, so just update it - templateCode = templateCode.replace(layoutDefRegex, "$1null$3"); - } - } - - vm.editor.setValue(templateCode); - vm.editor.clearSelection(); - vm.editor.navigateFileStart(); - - vm.editor.focus(); - // set form state to $dirty - setFormState("dirty"); - - } - - - function insert(str) { - vm.editor.focus(); - vm.editor.moveCursorToPosition(vm.currentPosition); - vm.editor.insert(str); - - // set form state to $dirty - setFormState("dirty"); - } - - function wrap(str) { - - var selectedContent = vm.editor.session.getTextRange(vm.editor.getSelectionRange()); - str = str.replace("{0}", selectedContent); - vm.editor.insert(str); - vm.editor.focus(); - - // set form state to $dirty - setFormState("dirty"); - } - - function persistCurrentLocation() { - vm.currentPosition = vm.editor.getCursorPosition(); - } - - function changeAceEditor() { - setFormState("dirty"); - } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if(state === "dirty") { - currentForm.$setDirty(); - } else if(state === "pristine") { - currentForm.$setPristine(); } + function clearAddUserForm() { + // clear form data + vm.newUser.name = ''; + vm.newUser.email = ''; + vm.newUser.userGroups = []; + vm.newUser.message = ''; + // clear button state + vm.page.createButtonState = 'init'; + } + init(); } - - vm.init(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.Templates.EditController", TemplatesEditController); -})(); - - -})(); \ No newline at end of file + angular.module('umbraco').controller('Umbraco.Editors.Users.UsersController', UsersController); + }()); +}()); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.directives.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.directives.js index 38041a41..38140121 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.directives.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.directives.js @@ -1,116 +1,97 @@ -/*! umbraco - * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2017 Umbraco HQ; - * Licensed - */ - -(function() { - -angular.module("umbraco.directives", ["umbraco.directives.editors", "umbraco.directives.html", "umbraco.directives.validation", "ui.sortable"]); -angular.module("umbraco.directives.editors", []); -angular.module("umbraco.directives.html", []); -angular.module("umbraco.directives.validation", []); -/** -* @ngdoc directive -* @name umbraco.directives.directive:autoScale -* @element div -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* @function -* @description -* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set -* So if you only want to scale the div to 70 pixels from the bottom you pass "70" - -* @example -* -* -*
-*
-*
+(function () { + angular.module('umbraco.directives', [ + 'umbraco.directives.editors', + 'umbraco.directives.html', + 'umbraco.directives.validation', + 'ui.sortable' + ]); + angular.module('umbraco.directives.editors', []); + angular.module('umbraco.directives.html', []); + angular.module('umbraco.directives.validation', []); + /** +* @ngdoc directive +* @name umbraco.directives.directive:autoScale +* @element div +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @function +* @description +* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set +* So if you only want to scale the div to 70 pixels from the bottom you pass "70" + +* @example +* +* +*
+*
+*
**/ - -angular.module("umbraco.directives") - .directive('autoScale', function ($window) { - return function (scope, el, attrs) { - - var totalOffset = 0; - var offsety = parseInt(attrs.autoScale, 10); - var window = angular.element($window); - if (offsety !== undefined){ - totalOffset += offsety; - } - - setTimeout(function () { - el.height(window.height() - (el.offset().top + totalOffset)); - }, 500); - - window.bind("resize", function () { - el.height(window.height() - (el.offset().top + totalOffset)); - }); - - }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:detectFold -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* @description This is used for the editor buttons to ensure they are displayed correctly if the horizontal overflow of the editor -* exceeds the height of the window + angular.module('umbraco.directives').directive('autoScale', function ($window) { + return function (scope, el, attrs) { + var totalOffset = 0; + var offsety = parseInt(attrs.autoScale, 10); + var window = angular.element($window); + if (offsety !== undefined) { + totalOffset += offsety; + } + setTimeout(function () { + el.height(window.height() - (el.offset().top + totalOffset)); + }, 500); + window.bind('resize', function () { + el.height(window.height() - (el.offset().top + totalOffset)); + }); + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:detectFold +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @description This is used for the editor buttons to ensure they are displayed correctly if the horizontal overflow of the editor +* exceeds the height of the window **/ - -angular.module("umbraco.directives.html") - .directive('detectFold', function ($timeout, $log, windowResizeListener) { - return { - require: "^?umbTabs", - restrict: 'A', - link: function (scope, el, attrs, tabsCtrl) { - - var firstRun = false; - var parent = $(".umb-panel-body"); - var winHeight = $(window).height(); - var calculate = function () { - if (el && el.is(":visible") && !el.hasClass("umb-bottom-bar")) { - - //now that the element is visible, set the flag in a couple of seconds, - // this will ensure that loading time of a current tab get's completed and that - // we eventually stop watching to save on CPU time - $timeout(function() { - firstRun = true; - }, 4000); - - //var parent = el.parent(); - var hasOverflow = parent.innerHeight() < parent[0].scrollHeight; - //var belowFold = (el.offset().top + el.height()) > winHeight; - if (hasOverflow) { - el.addClass("umb-bottom-bar"); - - //I wish we didn't have to put this logic here but unfortunately we - // do. This needs to calculate the left offest to place the bottom bar - // depending on if the left column splitter has been moved by the user + angular.module('umbraco.directives.html').directive('detectFold', function ($timeout, $log, windowResizeListener) { + return { + require: '^?umbTabs', + restrict: 'A', + link: function (scope, el, attrs, tabsCtrl) { + var firstRun = false; + var parent = $('.umb-panel-body'); + var winHeight = $(window).height(); + var calculate = function () { + if (el && el.is(':visible') && !el.hasClass('umb-bottom-bar')) { + //now that the element is visible, set the flag in a couple of seconds, + // this will ensure that loading time of a current tab get's completed and that + // we eventually stop watching to save on CPU time + $timeout(function () { + firstRun = true; + }, 4000); + //var parent = el.parent(); + var hasOverflow = parent.innerHeight() < parent[0].scrollHeight; + //var belowFold = (el.offset().top + el.height()) > winHeight; + if (hasOverflow) { + el.addClass('umb-bottom-bar'); + //I wish we didn't have to put this logic here but unfortunately we + // do. This needs to calculate the left offest to place the bottom bar + // depending on if the left column splitter has been moved by the user // (based on the nav-resize directive) - var wrapper = $("#mainwrapper"); - var contentPanel = $("#leftcolumn").next(); - var contentPanelLeftPx = contentPanel.css("left"); - - el.css({ left: contentPanelLeftPx }); - } - } - return firstRun; - }; - - var resizeCallback = function(size) { - winHeight = size.height; - el.removeClass("umb-bottom-bar"); - calculate(); - }; - - windowResizeListener.register(resizeCallback); - - //Only execute the watcher if this tab is the active (first) tab on load, otherwise there's no reason to execute - // the watcher since it will be recalculated when the tab changes! - if (el.closest(".umb-tab-pane").index() === 0) { + var wrapper = $('#mainwrapper'); + var contentPanel = $('#leftcolumn').next(); + var contentPanelLeftPx = contentPanel.css('left'); + el.css({ left: contentPanelLeftPx }); + } + } + return firstRun; + }; + var resizeCallback = function (size) { + winHeight = size.height; + el.removeClass('umb-bottom-bar'); + calculate(); + }; + windowResizeListener.register(resizeCallback); + //Only execute the watcher if this tab is the active (first) tab on load, otherwise there's no reason to execute + // the watcher since it will be recalculated when the tab changes! + if (el.closest('.umb-tab-pane').index() === 0) { //run a watcher to ensure that the calculation occurs until it's firstRun but ensure // the calculations are throttled to save a bit of CPU var listener = scope.$watch(_.throttle(calculate, 1000), function (newVal, oldVal) { @@ -119,250 +100,211 @@ angular.module("umbraco.directives.html") } }); } - - //listen for tab changes + //listen for tab changes if (tabsCtrl != null) { tabsCtrl.onTabShown(function (args) { calculate(); }); } - - //ensure to unregister - scope.$on('$destroy', function() { - windowResizeListener.unregister(resizeCallback); - }); - } - }; - }); -/** + //ensure to unregister + scope.$on('$destroy', function () { + windowResizeListener.unregister(resizeCallback); + }); + } + }; + }); + /** * @ngdoc directive -* @name umbraco.directives.directive:umbItemSorter +* @name umbraco.directives.directive:umbContentName * @deprecated * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* @function -* @element ANY * @restrict E -* @description A re-usable directive for sorting items -**/ - -function umbItemSorter(angularHelper) { - return { - scope: { - model: "=" - }, - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/directives/_obsolete/umb-item-sorter.html', - link: function(scope, element, attrs, ctrl) { - var defaultModel = { - okButton: "Ok", - successMsg: "Sorting successful", - complete: false - }; - //assign user vals to default - angular.extend(defaultModel, scope.model); - //re-assign merged to user - scope.model = defaultModel; - - scope.performSort = function() { - scope.$emit("umbItemSorter.sorting", { - sortedItems: scope.model.itemsToSort - }); - }; - - scope.handleCancel = function () { - scope.$emit("umbItemSorter.cancel"); - }; - - scope.handleOk = function() { - scope.$emit("umbItemSorter.ok"); - }; - - //defines the options for the jquery sortable - scope.sortableOptions = { - axis: 'y', - cursor: "move", - placeholder: "ui-sortable-placeholder", - update: function (ev, ui) { - //highlight the item when the position is changed - $(ui.item).effect("highlight", { color: "#049cdb" }, 500); - }, - stop: function (ev, ui) { - //the ui-sortable directive already ensures that our list is re-sorted, so now we just - // need to update the sortOrder to the index of each item - angularHelper.safeApply(scope, function () { - angular.forEach(scope.itemsToSort, function (val, index) { - val.sortOrder = index + 1; - }); - - }); - } - }; - } - }; -} - -angular.module('umbraco.directives').directive("umbItemSorter", umbItemSorter); - -/** +* @function +* @description +* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. +**/ + angular.module('umbraco.directives').directive('umbContentName', function ($timeout, localizationService) { + return { + require: 'ngModel', + restrict: 'E', + replace: true, + templateUrl: 'views/directives/_obsolete/umb-content-name.html', + scope: { + placeholder: '@placeholder', + model: '=ngModel', + ngDisabled: '=' + }, + link: function (scope, element, attrs, ngModel) { + var inputElement = element.find('input'); + if (scope.placeholder && scope.placeholder[0] === '@') { + localizationService.localize(scope.placeholder.substring(1)).then(function (value) { + scope.placeholder = value; + }); + } + var mX, mY, distance; + function calculateDistance(elem, mouseX, mouseY) { + var cx = Math.max(Math.min(mouseX, elem.offset().left + elem.width()), elem.offset().left); + var cy = Math.max(Math.min(mouseY, elem.offset().top + elem.height()), elem.offset().top); + return Math.sqrt((mouseX - cx) * (mouseX - cx) + (mouseY - cy) * (mouseY - cy)); + } + var mouseMoveDebounce = _.throttle(function (e) { + mX = e.pageX; + mY = e.pageY; + // not focused and not over element + if (!inputElement.is(':focus') && !inputElement.hasClass('ng-invalid')) { + // on page + if (mX >= inputElement.offset().left) { + distance = calculateDistance(inputElement, mX, mY); + if (distance <= 155) { + distance = 1 - 100 / 150 * distance / 100; + inputElement.css('border', '1px solid rgba(175,175,175, ' + distance + ')'); + inputElement.css('background-color', 'rgba(255,255,255, ' + distance + ')'); + } + } + } + }, 15); + $(document).bind('mousemove', mouseMoveDebounce); + $timeout(function () { + if (!scope.model) { + scope.goEdit(); + } + }, 100, false); + scope.goEdit = function () { + scope.editMode = true; + $timeout(function () { + inputElement.focus(); + }, 100, false); + }; + scope.exitEdit = function () { + if (scope.model && scope.model !== '') { + scope.editMode = false; + } + }; + //unbind doc event! + scope.$on('$destroy', function () { + $(document).unbind('mousemove', mouseMoveDebounce); + }); + } + }; + }); + /** * @ngdoc directive -* @name umbraco.directives.directive:umbContentName +* @name umbraco.directives.directive:umbHeader * @deprecated * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. * @restrict E * @function * @description -* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. -**/ - -angular.module("umbraco.directives") - .directive('umbContentName', function ($timeout, localizationService) { - return { - require: "ngModel", - restrict: 'E', - replace: true, - templateUrl: 'views/directives/_obsolete/umb-content-name.html', - - scope: { - placeholder: '@placeholder', - model: '=ngModel', - ngDisabled: '=' - }, - link: function(scope, element, attrs, ngModel) { - - var inputElement = element.find("input"); - if(scope.placeholder && scope.placeholder[0] === "@"){ - localizationService.localize(scope.placeholder.substring(1)) - .then(function(value){ - scope.placeholder = value; - }); - } - - var mX, mY, distance; - - function calculateDistance(elem, mouseX, mouseY) { - - var cx = Math.max(Math.min(mouseX, elem.offset().left + elem.width()), elem.offset().left); - var cy = Math.max(Math.min(mouseY, elem.offset().top + elem.height()), elem.offset().top); - return Math.sqrt((mouseX - cx) * (mouseX - cx) + (mouseY - cy) * (mouseY - cy)); - } - - var mouseMoveDebounce = _.throttle(function (e) { - mX = e.pageX; - mY = e.pageY; - // not focused and not over element - if (!inputElement.is(":focus") && !inputElement.hasClass("ng-invalid")) { - // on page - if (mX >= inputElement.offset().left) { - distance = calculateDistance(inputElement, mX, mY); - if (distance <= 155) { - - distance = 1 - (100 / 150 * distance / 100); - inputElement.css("border", "1px solid rgba(175,175,175, " + distance + ")"); - inputElement.css("background-color", "rgba(255,255,255, " + distance + ")"); - } - } - - } - - }, 15); - - $(document).bind("mousemove", mouseMoveDebounce); - - $timeout(function(){ - if(!scope.model){ - scope.goEdit(); - } - }, 100, false); - - scope.goEdit = function(){ - scope.editMode = true; - - $timeout(function () { - inputElement.focus(); - }, 100, false); - }; - - scope.exitEdit = function(){ - if(scope.model && scope.model !== ""){ - scope.editMode = false; - } - }; - - //unbind doc event! - scope.$on('$destroy', function () { - $(document).unbind("mousemove", mouseMoveDebounce); - }); - } - }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbHeader -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* @restrict E -* @function -* @description -* The header on an editor that contains tabs using bootstrap tabs - THIS IS OBSOLETE, use umbTabHeader instead +* The header on an editor that contains tabs using bootstrap tabs - THIS IS OBSOLETE, use umbTabHeader instead **/ - -angular.module("umbraco.directives") -.directive('umbHeader', function ($parse, $timeout) { - return { - restrict: 'E', - replace: true, - transclude: 'true', - templateUrl: 'views/directives/_obsolete/umb-header.html', - //create a new isolated scope assigning a tabs property from the attribute 'tabs' - //which is bound to the parent scope property passed in - scope: { - tabs: "=" - }, - link: function (scope, iElement, iAttrs) { - - scope.showTabs = iAttrs.tabs ? true : false; - scope.visibleTabs = []; - - //since tabs are loaded async, we need to put a watch on them to determine - // when they are loaded, then we can close the watch - var tabWatch = scope.$watch("tabs", function (newValue, oldValue) { - - angular.forEach(newValue, function(val, index){ - var tab = {id: val.id, label: val.label}; + angular.module('umbraco.directives').directive('umbHeader', function ($parse, $timeout) { + return { + restrict: 'E', + replace: true, + transclude: 'true', + templateUrl: 'views/directives/_obsolete/umb-header.html', + //create a new isolated scope assigning a tabs property from the attribute 'tabs' + //which is bound to the parent scope property passed in + scope: { tabs: '=' }, + link: function (scope, iElement, iAttrs) { + scope.showTabs = iAttrs.tabs ? true : false; + scope.visibleTabs = []; + //since tabs are loaded async, we need to put a watch on them to determine + // when they are loaded, then we can close the watch + var tabWatch = scope.$watch('tabs', function (newValue, oldValue) { + angular.forEach(newValue, function (val, index) { + var tab = { + id: val.id, + label: val.label + }; scope.visibleTabs.push(tab); - }); - - //don't process if we cannot or have already done so - if (!newValue) {return;} - if (!newValue.length || newValue.length === 0){return;} - - //we need to do a timeout here so that the current sync operation can complete - // and update the UI, then this will fire and the UI elements will be available. - $timeout(function () { - - //use bootstrap tabs API to show the first one - iElement.find(".nav-tabs a:first").tab('show'); - - //enable the tab drop - iElement.find('.nav-pills, .nav-tabs').tabdrop(); - - //ensure to destroy tabdrop (unbinds window resize listeners) - scope.$on('$destroy', function () { - iElement.find('.nav-pills, .nav-tabs').tabdrop("destroy"); }); - - //stop watching now - tabWatch(); - }, 200); - - }); - } - }; -}); - -/** + //don't process if we cannot or have already done so + if (!newValue) { + return; + } + if (!newValue.length || newValue.length === 0) { + return; + } + //we need to do a timeout here so that the current sync operation can complete + // and update the UI, then this will fire and the UI elements will be available. + $timeout(function () { + //use bootstrap tabs API to show the first one + iElement.find('.nav-tabs a:first').tab('show'); + //enable the tab drop + iElement.find('.nav-pills, .nav-tabs').tabdrop(); + //ensure to destroy tabdrop (unbinds window resize listeners) + scope.$on('$destroy', function () { + iElement.find('.nav-pills, .nav-tabs').tabdrop('destroy'); + }); + //stop watching now + tabWatch(); + }, 200); + }); + } + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbItemSorter +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @function +* @element ANY +* @restrict E +* @description A re-usable directive for sorting items +**/ + function umbItemSorter(angularHelper) { + return { + scope: { model: '=' }, + restrict: 'E', + // restrict to an element + replace: true, + // replace the html element with the template + templateUrl: 'views/directives/_obsolete/umb-item-sorter.html', + link: function (scope, element, attrs, ctrl) { + var defaultModel = { + okButton: 'Ok', + successMsg: 'Sorting successful', + complete: false + }; + //assign user vals to default + angular.extend(defaultModel, scope.model); + //re-assign merged to user + scope.model = defaultModel; + scope.performSort = function () { + scope.$emit('umbItemSorter.sorting', { sortedItems: scope.model.itemsToSort }); + }; + scope.handleCancel = function () { + scope.$emit('umbItemSorter.cancel'); + }; + scope.handleOk = function () { + scope.$emit('umbItemSorter.ok'); + }; + //defines the options for the jquery sortable + scope.sortableOptions = { + axis: 'y', + cursor: 'move', + placeholder: 'ui-sortable-placeholder', + update: function (ev, ui) { + //highlight the item when the position is changed + $(ui.item).effect('highlight', { color: '#049cdb' }, 500); + }, + stop: function (ev, ui) { + //the ui-sortable directive already ensures that our list is re-sorted, so now we just + // need to update the sortOrder to the index of each item + angularHelper.safeApply(scope, function () { + angular.forEach(scope.itemsToSort, function (val, index) { + val.sortOrder = index + 1; + }); + }); + } + }; + } + }; + } + angular.module('umbraco.directives').directive('umbItemSorter', umbItemSorter); + /** * @ngdoc directive * @name umbraco.directives.directive:umbLogin * @deprecated @@ -370,140 +312,111 @@ angular.module("umbraco.directives") * @function * @element ANY * @restrict E -**/ - -function loginDirective() { - return { - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/directives/_obsolete/umb-login.html' - }; -} - -angular.module('umbraco.directives').directive("umbLogin", loginDirective); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbOptionsMenu -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* @function -* @element ANY -* @restrict E **/ - -angular.module("umbraco.directives") -.directive('umbOptionsMenu', function ($injector, treeService, navigationService, umbModelMapper, appState) { - return { - scope: { - currentSection: "@", - currentNode: "=" - }, - restrict: 'E', - replace: true, - templateUrl: 'views/directives/_obsolete/umb-optionsmenu.html', - link: function (scope, element, attrs, ctrl) { - - //adds a handler to the context menu item click, we need to handle this differently - //depending on what the menu item is supposed to do. - scope.executeMenuItem = function (action) { - navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); - }; - - //callback method to go and get the options async - scope.getOptions = function () { - - if (!scope.currentNode) { - return; - } - - //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) - appState.setMenuState("currentNode", scope.currentNode); - - if (!scope.actions) { - treeService.getMenu({ treeNode: scope.currentNode }) - .then(function (data) { + function loginDirective() { + return { + restrict: 'E', + // restrict to an element + replace: true, + // replace the html element with the template + templateUrl: 'views/directives/_obsolete/umb-login.html' + }; + } + angular.module('umbraco.directives').directive('umbLogin', loginDirective); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbOptionsMenu +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @function +* @element ANY +* @restrict E +**/ + angular.module('umbraco.directives').directive('umbOptionsMenu', function ($injector, treeService, navigationService, umbModelMapper, appState) { + return { + scope: { + currentSection: '@', + currentNode: '=' + }, + restrict: 'E', + replace: true, + templateUrl: 'views/directives/_obsolete/umb-optionsmenu.html', + link: function (scope, element, attrs, ctrl) { + //adds a handler to the context menu item click, we need to handle this differently + //depending on what the menu item is supposed to do. + scope.executeMenuItem = function (action) { + navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); + }; + //callback method to go and get the options async + scope.getOptions = function () { + if (!scope.currentNode) { + return; + } + //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) + appState.setMenuState('currentNode', scope.currentNode); + if (!scope.actions) { + treeService.getMenu({ treeNode: scope.currentNode }).then(function (data) { scope.actions = data.menuItems; }); - } - }; - - } - }; -}); - -/** + } + }; + } + }; + }); + /** * @ngdoc directive * @name umbraco.directives.directive:umbPhotoFolder * @deprecated * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. * @restrict E -**/ - -angular.module("umbraco.directives.html") - .directive('umbPhotoFolder', function($compile, $log, $timeout, $filter, umbPhotoFolderHelper) { - - return { - restrict: 'E', - replace: true, - require: '?ngModel', - terminate: true, - templateUrl: 'views/directives/_obsolete/umb-photo-folder.html', - link: function(scope, element, attrs, ngModel) { - - var lastWatch = null; - - ngModel.$render = function() { - if (ngModel.$modelValue) { - - $timeout(function() { - var photos = ngModel.$modelValue; - - scope.clickHandler = scope.$eval(element.attr('on-click')); - - - var imagesOnly = element.attr('images-only') === "true"; - - - var margin = element.attr('border') ? parseInt(element.attr('border'), 10) : 5; - var startingIndex = element.attr('baseline') ? parseInt(element.attr('baseline'), 10) : 0; - var minWidth = element.attr('min-width') ? parseInt(element.attr('min-width'), 10) : 420; - var minHeight = element.attr('min-height') ? parseInt(element.attr('min-height'), 10) : 100; - var maxHeight = element.attr('max-height') ? parseInt(element.attr('max-height'), 10) : 300; - var idealImgPerRow = element.attr('ideal-items-per-row') ? parseInt(element.attr('ideal-items-per-row'), 10) : 5; - var fixedRowWidth = Math.max(element.width(), minWidth); - - scope.containerStyle = { width: fixedRowWidth + "px" }; - scope.rows = umbPhotoFolderHelper.buildGrid(photos, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly); - - if (attrs.filterBy) { - - //we track the watches that we create, we don't want to create multiple, so clear it - // if it already exists before creating another. - if (lastWatch) { - lastWatch(); - } - - //TODO: Need to debounce this so it doesn't filter too often! - lastWatch = scope.$watch(attrs.filterBy, function (newVal, oldVal) { - if (newVal && newVal !== oldVal) { - var p = $filter('filter')(photos, newVal, false); - scope.baseline = 0; - var m = umbPhotoFolderHelper.buildGrid(p, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly); - scope.rows = m; - } - }); - } - - }, 500); //end timeout - } //end if modelValue - - }; //end $render - } - }; - }); - -/** +**/ + angular.module('umbraco.directives.html').directive('umbPhotoFolder', function ($compile, $log, $timeout, $filter, umbPhotoFolderHelper) { + return { + restrict: 'E', + replace: true, + require: '?ngModel', + terminate: true, + templateUrl: 'views/directives/_obsolete/umb-photo-folder.html', + link: function (scope, element, attrs, ngModel) { + var lastWatch = null; + ngModel.$render = function () { + if (ngModel.$modelValue) { + $timeout(function () { + var photos = ngModel.$modelValue; + scope.clickHandler = scope.$eval(element.attr('on-click')); + var imagesOnly = element.attr('images-only') === 'true'; + var margin = element.attr('border') ? parseInt(element.attr('border'), 10) : 5; + var startingIndex = element.attr('baseline') ? parseInt(element.attr('baseline'), 10) : 0; + var minWidth = element.attr('min-width') ? parseInt(element.attr('min-width'), 10) : 420; + var minHeight = element.attr('min-height') ? parseInt(element.attr('min-height'), 10) : 100; + var maxHeight = element.attr('max-height') ? parseInt(element.attr('max-height'), 10) : 300; + var idealImgPerRow = element.attr('ideal-items-per-row') ? parseInt(element.attr('ideal-items-per-row'), 10) : 5; + var fixedRowWidth = Math.max(element.width(), minWidth); + scope.containerStyle = { width: fixedRowWidth + 'px' }; + scope.rows = umbPhotoFolderHelper.buildGrid(photos, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly); + if (attrs.filterBy) { + //we track the watches that we create, we don't want to create multiple, so clear it + // if it already exists before creating another. + if (lastWatch) { + lastWatch(); + } + //TODO: Need to debounce this so it doesn't filter too often! + lastWatch = scope.$watch(attrs.filterBy, function (newVal, oldVal) { + if (newVal && newVal !== oldVal) { + var p = $filter('filter')(photos, newVal, false); + scope.baseline = 0; + var m = umbPhotoFolderHelper.buildGrid(p, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly); + scope.rows = m; + } + }); + } + }, 500); //end timeout + } //end if modelValue + }; //end $render + } + }; + }); + /** * @ngdoc directive * @name umbraco.directives.directive:umbSort * @deprecated @@ -522,30 +435,18 @@ angular.module("umbraco.directives.html") *
* * -**/ - -angular.module("umbraco.directives") - .value('umbSortContextInternal',{}) - .directive('umbSort', function($log,umbSortContextInternal) { - return { - require: '?ngModel', - link: function(scope, element, attrs, ngModel) { - var adjustment; - - var cfg = scope.$eval(element.attr('umb-sort')) || {}; - - scope.model = ngModel; - - scope.opts = cfg; - scope.opts.containerSelector= cfg.containerSelector || ".umb-" + cfg.group + "-container", - scope.opts.nested= cfg.nested || true, - scope.opts.drop= cfg.drop || true, - scope.opts.drag= cfg.drag || true, - scope.opts.clone = cfg.clone || "
  • "; - scope.opts.mode = cfg.mode || "list"; - - scope.opts.itemSelectorFull = $.trim(scope.opts.itemPath + " " + scope.opts.itemSelector); - +**/ + angular.module('umbraco.directives').value('umbSortContextInternal', {}).directive('umbSort', function ($log, umbSortContextInternal) { + return { + require: '?ngModel', + link: function (scope, element, attrs, ngModel) { + var adjustment; + var cfg = scope.$eval(element.attr('umb-sort')) || {}; + scope.model = ngModel; + scope.opts = cfg; + scope.opts.containerSelector = cfg.containerSelector || '.umb-' + cfg.group + '-container', scope.opts.nested = cfg.nested || true, scope.opts.drop = cfg.drop || true, scope.opts.drag = cfg.drag || true, scope.opts.clone = cfg.clone || '
  • '; + scope.opts.mode = cfg.mode || 'list'; + scope.opts.itemSelectorFull = $.trim(scope.opts.itemPath + ' ' + scope.opts.itemSelector); /* scope.opts.isValidTarget = function(item, container) { if(container.el.is(".umb-" + scope.opts.group + "-container")){ @@ -553,5839 +454,6984 @@ angular.module("umbraco.directives") } return false; }; - */ - - element.addClass("umb-sort"); - element.addClass("umb-" + cfg.group + "-container"); - - scope.opts.onDrag = function (item, position) { - if(scope.opts.mode === "list"){ - item.css({ - left: position.left - adjustment.left, - top: position.top - adjustment.top - }); - } - }; - - - scope.opts.onDrop = function (item, targetContainer, _super) { - - if(scope.opts.mode === "list"){ - //list mode - var clonedItem = $(scope.opts.clone).css({height: 0}); - item.after(clonedItem); - clonedItem.animate({'height': item.height()}); - - item.animate(clonedItem.position(), function () { - clonedItem.detach(); - _super(item); - }); - } - - var children = $(scope.opts.itemSelectorFull, targetContainer.el); - var targetIndex = children.index(item); - var targetScope = $(targetContainer.el[0]).scope(); - - - if(targetScope === umbSortContextInternal.sourceScope){ - if(umbSortContextInternal.sourceScope.opts.onSortHandler){ - var _largs = { - oldIndex: umbSortContextInternal.sourceIndex, - newIndex: targetIndex, - scope: umbSortContextInternal.sourceScope - }; - - umbSortContextInternal.sourceScope.opts.onSortHandler.call(this, item, _largs); - } - }else{ - - - if(targetScope.opts.onDropHandler){ - var args = { - sourceScope: umbSortContextInternal.sourceScope, - sourceIndex: umbSortContextInternal.sourceIndex, - sourceContainer: umbSortContextInternal.sourceContainer, - - targetScope: targetScope, - targetIndex: targetIndex, - targetContainer: targetContainer - }; - - targetScope.opts.onDropHandler.call(this, item, args); - } - - if(umbSortContextInternal.sourceScope.opts.onReleaseHandler){ - var _args = { - sourceScope: umbSortContextInternal.sourceScope, - sourceIndex: umbSortContextInternal.sourceIndex, - sourceContainer: umbSortContextInternal.sourceContainer, - - targetScope: targetScope, - targetIndex: targetIndex, - targetContainer: targetContainer - }; - - umbSortContextInternal.sourceScope.opts.onReleaseHandler.call(this, item, _args); - } - } - }; - - scope.changeIndex = function(from, to){ - scope.$apply(function(){ - var i = ngModel.$modelValue.splice(from, 1)[0]; - ngModel.$modelValue.splice(to, 0, i); - }); - }; - - scope.move = function(args){ - var from = args.sourceIndex; - var to = args.targetIndex; - - if(args.sourceContainer === args.targetContainer){ - scope.changeIndex(from, to); - }else{ - scope.$apply(function(){ - var i = args.sourceScope.model.$modelValue.splice(from, 1)[0]; - args.targetScope.model.$modelvalue.splice(to,0, i); - }); - } - }; - - scope.opts.onDragStart = function (item, container, _super) { - var children = $(scope.opts.itemSelectorFull, container.el); - var offset = item.offset(); - - umbSortContextInternal.sourceIndex = children.index(item); - umbSortContextInternal.sourceScope = $(container.el[0]).scope(); - umbSortContextInternal.sourceContainer = container; - - //current.item = ngModel.$modelValue.splice(current.index, 1)[0]; - - var pointer = container.rootGroup.pointer; - adjustment = { - left: pointer.left - offset.left, - top: pointer.top - offset.top - }; - - _super(item, container); - }; - - element.sortable( scope.opts ); - } - }; - - }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTabView -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* -* @restrict E -**/ - -angular.module("umbraco.directives") -.directive('umbTabView', function($timeout, $log){ - return { - restrict: 'E', - replace: true, - transclude: 'true', - templateUrl: 'views/directives/_obsolete/umb-tab-view.html' - }; -}); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbUploadDropzone -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* -* @restrict E -**/ - -angular.module("umbraco.directives.html") - .directive('umbUploadDropzone', function(){ - return { - restrict: 'E', - replace: true, - templateUrl: 'views/directives/_obsolete/umb-upload-dropzone.html' - }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:navResize -* @restrict A - * - * @description - * Handles how the navigation responds to window resizing and controls how the draggable resize panel works -**/ -angular.module("umbraco.directives") - .directive('navResize', function (appState, eventsService, windowResizeListener) { - return { - restrict: 'A', - link: function (scope, element, attrs, ctrl) { - - var minScreenSize = 1100; - var resizeEnabled = false; - - function setTreeMode() { - appState.setGlobalState("showNavigation", appState.getGlobalState("isTablet") === false); - } - - function enableResize() { - //only enable when the size is correct and it's not already enabled - if (!resizeEnabled && appState.getGlobalState("isTablet") === false) { - element.resizable( - { - containment: $("#mainwrapper"), - autoHide: true, - handles: "e", - alsoResize: ".navigation-inner-container", - resize: function(e, ui) { - var wrapper = $("#mainwrapper"); - var contentPanel = $("#contentwrapper"); - var umbNotification = $("#umb-notifications-wrapper"); - var apps = $("#applications"); - var bottomBar = contentPanel.find(".umb-bottom-bar"); - var navOffeset = $("#navOffset"); - - var leftPanelWidth = ui.element.width() + apps.width(); - - contentPanel.css({ left: leftPanelWidth }); - bottomBar.css({ left: leftPanelWidth }); - umbNotification.css({ left: leftPanelWidth }); - - navOffeset.css({ "margin-left": ui.element.outerWidth() }); - }, - stop: function (e, ui) { - - } + */ + element.addClass('umb-sort'); + element.addClass('umb-' + cfg.group + '-container'); + scope.opts.onDrag = function (item, position) { + if (scope.opts.mode === 'list') { + item.css({ + left: position.left - adjustment.left, + top: position.top - adjustment.top }); - - resizeEnabled = true; } - } - - function resetResize() { - if (resizeEnabled) { - //kill the resize - element.resizable("destroy"); - element.css("width", ""); - - var navInnerContainer = element.find(".navigation-inner-container"); - - navInnerContainer.css("width", ""); - $("#contentwrapper").css("left", ""); - $("#umb-notifications-wrapper").css("left", ""); - $("#navOffset").css("margin-left", ""); - - resizeEnabled = false; + }; + scope.opts.onDrop = function (item, targetContainer, _super) { + if (scope.opts.mode === 'list') { + //list mode + var clonedItem = $(scope.opts.clone).css({ height: 0 }); + item.after(clonedItem); + clonedItem.animate({ 'height': item.height() }); + item.animate(clonedItem.position(), function () { + clonedItem.detach(); + _super(item); + }); } - } - - var evts = []; - - //Listen for global state changes - evts.push(eventsService.on("appState.globalState.changed", function (e, args) { - if (args.key === "showNavigation") { - if (args.value === false) { - resetResize(); + var children = $(scope.opts.itemSelectorFull, targetContainer.el); + var targetIndex = children.index(item); + var targetScope = $(targetContainer.el[0]).scope(); + if (targetScope === umbSortContextInternal.sourceScope) { + if (umbSortContextInternal.sourceScope.opts.onSortHandler) { + var _largs = { + oldIndex: umbSortContextInternal.sourceIndex, + newIndex: targetIndex, + scope: umbSortContextInternal.sourceScope + }; + umbSortContextInternal.sourceScope.opts.onSortHandler.call(this, item, _largs); } - else { - enableResize(); + } else { + if (targetScope.opts.onDropHandler) { + var args = { + sourceScope: umbSortContextInternal.sourceScope, + sourceIndex: umbSortContextInternal.sourceIndex, + sourceContainer: umbSortContextInternal.sourceContainer, + targetScope: targetScope, + targetIndex: targetIndex, + targetContainer: targetContainer + }; + targetScope.opts.onDropHandler.call(this, item, args); + } + if (umbSortContextInternal.sourceScope.opts.onReleaseHandler) { + var _args = { + sourceScope: umbSortContextInternal.sourceScope, + sourceIndex: umbSortContextInternal.sourceIndex, + sourceContainer: umbSortContextInternal.sourceContainer, + targetScope: targetScope, + targetIndex: targetIndex, + targetContainer: targetContainer + }; + umbSortContextInternal.sourceScope.opts.onReleaseHandler.call(this, item, _args); + } + } + }; + scope.changeIndex = function (from, to) { + scope.$apply(function () { + var i = ngModel.$modelValue.splice(from, 1)[0]; + ngModel.$modelValue.splice(to, 0, i); + }); + }; + scope.move = function (args) { + var from = args.sourceIndex; + var to = args.targetIndex; + if (args.sourceContainer === args.targetContainer) { + scope.changeIndex(from, to); + } else { + scope.$apply(function () { + var i = args.sourceScope.model.$modelValue.splice(from, 1)[0]; + args.targetScope.model.$modelvalue.splice(to, 0, i); + }); + } + }; + scope.opts.onDragStart = function (item, container, _super) { + var children = $(scope.opts.itemSelectorFull, container.el); + var offset = item.offset(); + umbSortContextInternal.sourceIndex = children.index(item); + umbSortContextInternal.sourceScope = $(container.el[0]).scope(); + umbSortContextInternal.sourceContainer = container; + //current.item = ngModel.$modelValue.splice(current.index, 1)[0]; + var pointer = container.rootGroup.pointer; + adjustment = { + left: pointer.left - offset.left, + top: pointer.top - offset.top + }; + _super(item, container); + }; + element.sortable(scope.opts); + } + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbTabView +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* +* @restrict E +**/ + angular.module('umbraco.directives').directive('umbTabView', function ($timeout, $log) { + return { + restrict: 'E', + replace: true, + transclude: 'true', + templateUrl: 'views/directives/_obsolete/umb-tab-view.html' + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbUploadDropzone +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* +* @restrict E +**/ + angular.module('umbraco.directives.html').directive('umbUploadDropzone', function () { + return { + restrict: 'E', + replace: true, + templateUrl: 'views/directives/_obsolete/umb-upload-dropzone.html' + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:navResize +* @restrict A + * + * @description + * Handles how the navigation responds to window resizing and controls how the draggable resize panel works +**/ + angular.module('umbraco.directives').directive('navResize', function (appState, eventsService, windowResizeListener) { + return { + restrict: 'A', + link: function (scope, element, attrs, ctrl) { + var minScreenSize = 1100; + var resizeEnabled = false; + function setTreeMode() { + appState.setGlobalState('showNavigation', appState.getGlobalState('isTablet') === false); + } + function enableResize() { + //only enable when the size is correct and it's not already enabled + if (!resizeEnabled && appState.getGlobalState('isTablet') === false) { + element.resizable({ + containment: $('#mainwrapper'), + autoHide: true, + handles: 'e', + alsoResize: '.navigation-inner-container', + resize: function (e, ui) { + var wrapper = $('#mainwrapper'); + var contentPanel = $('#contentwrapper'); + var umbNotification = $('#umb-notifications-wrapper'); + var apps = $('#applications'); + var bottomBar = contentPanel.find('.umb-bottom-bar'); + var navOffeset = $('#navOffset'); + var leftPanelWidth = ui.element.width() + apps.width(); + contentPanel.css({ left: leftPanelWidth }); + bottomBar.css({ left: leftPanelWidth }); + umbNotification.css({ left: leftPanelWidth }); + navOffeset.css({ 'margin-left': ui.element.outerWidth() }); + }, + stop: function (e, ui) { + } + }); + resizeEnabled = true; + } + } + function resetResize() { + if (resizeEnabled) { + //kill the resize + element.resizable('destroy'); + element.css('width', ''); + var navInnerContainer = element.find('.navigation-inner-container'); + navInnerContainer.css('width', ''); + $('#contentwrapper').css('left', ''); + $('#umb-notifications-wrapper').css('left', ''); + $('#navOffset').css('margin-left', ''); + resizeEnabled = false; + } + } + var evts = []; + //Listen for global state changes + evts.push(eventsService.on('appState.globalState.changed', function (e, args) { + if (args.key === 'showNavigation') { + if (args.value === false) { + resetResize(); + } else { + enableResize(); } } })); - - var resizeCallback = function(size) { + var resizeCallback = function (size) { //set the global app state - appState.setGlobalState("isTablet", (size.width <= minScreenSize)); + appState.setGlobalState('isTablet', size.width <= minScreenSize); setTreeMode(); }; - windowResizeListener.register(resizeCallback); - //ensure to unregister from all events and kill jquery plugins scope.$on('$destroy', function () { windowResizeListener.unregister(resizeCallback); for (var e in evts) { eventsService.unsubscribe(evts[e]); } - var navInnerContainer = element.find(".navigation-inner-container"); - navInnerContainer.resizable("destroy"); + var navInnerContainer = element.find('.navigation-inner-container'); + navInnerContainer.resizable('destroy'); }); - //init //set the global app state - appState.setGlobalState("isTablet", ($(window).width() <= minScreenSize)); + appState.setGlobalState('isTablet', $(window).width() <= minScreenSize); setTreeMode(); } }; }); - -angular.module("umbraco.directives") -.directive('sectionIcon', function ($compile, iconHelper) { - return { - restrict: 'E', - replace: true, - - link: function (scope, element, attrs) { - - var icon = attrs.icon; - - if (iconHelper.isLegacyIcon(icon)) { - //its a known legacy icon, convert to a new one - element.html(""); - } - else if (iconHelper.isFileBasedIcon(icon)) { - var convert = iconHelper.convertFromLegacyImage(icon); - if(convert){ - element.html(""); - }else{ - element.html(""); - } - //it's a file, normally legacy so look in the icon tray images + angular.module('umbraco.directives').directive('sectionIcon', function ($compile, iconHelper) { + return { + restrict: 'E', + replace: true, + link: function (scope, element, attrs) { + var icon = attrs.icon; + if (iconHelper.isLegacyIcon(icon)) { + //its a known legacy icon, convert to a new one + element.html(''); + } else if (iconHelper.isFileBasedIcon(icon)) { + var convert = iconHelper.convertFromLegacyImage(icon); + if (convert) { + element.html(''); + } else { + element.html(''); + } //it's a file, normally legacy so look in the icon tray images + } else { + //it's normal + element.html(''); + } } - else { - //it's normal - element.html(""); + }; + }); + angular.module('umbraco.directives').directive('umbContextMenu', function (navigationService) { + return { + scope: { + menuDialogTitle: '@', + currentSection: '@', + currentNode: '=', + menuActions: '=' + }, + restrict: 'E', + replace: true, + templateUrl: 'views/components/application/umb-contextmenu.html', + link: function (scope, element, attrs, ctrl) { + //adds a handler to the context menu item click, we need to handle this differently + //depending on what the menu item is supposed to do. + scope.executeMenuItem = function (action) { + navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); + }; } - } - }; -}); -angular.module("umbraco.directives") -.directive('umbContextMenu', function (navigationService) { - return { - scope: { - menuDialogTitle: "@", - currentSection: "@", - currentNode: "=", - menuActions: "=" - }, - restrict: 'E', - replace: true, - templateUrl: 'views/components/application/umb-contextmenu.html', - link: function (scope, element, attrs, ctrl) { - - //adds a handler to the context menu item click, we need to handle this differently - //depending on what the menu item is supposed to do. - scope.executeMenuItem = function (action) { - navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); - }; - } - }; -}); - -/** + }; + }); + /** * @ngdoc directive * @name umbraco.directives.directive:umbNavigation * @restrict E -**/ -function umbNavigationDirective() { - return { - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/components/application/umb-navigation.html' - }; -} - -angular.module('umbraco.directives').directive("umbNavigation", umbNavigationDirective); - -/** +**/ + function umbNavigationDirective() { + return { + restrict: 'E', + // restrict to an element + replace: true, + // replace the html element with the template + templateUrl: 'views/components/application/umb-navigation.html' + }; + } + angular.module('umbraco.directives').directive('umbNavigation', umbNavigationDirective); + /** * @ngdoc directive * @name umbraco.directives.directive:umbSections * @restrict E -**/ -function sectionsDirective($timeout, $window, navigationService, treeService, sectionResource, appState, eventsService, $location) { - return { - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/components/application/umb-sections.html', - link: function (scope, element, attr, ctrl) { - - //setup scope vars - scope.maxSections = 7; - scope.overflowingSections = 0; - scope.sections = []; - scope.currentSection = appState.getSectionState("currentSection"); - scope.showTray = false; //appState.getGlobalState("showTray"); - scope.stickyNavigation = appState.getGlobalState("stickyNavigation"); - scope.needTray = false; - scope.trayAnimation = function() { - if (scope.showTray) { - return 'slide'; - } - else if (scope.showTray === false) { - return 'slide'; - } - else { - return ''; - } - }; - - function loadSections(){ - sectionResource.getSections() - .then(function (result) { - scope.sections = result; - calculateHeight(); - }); - } - - function calculateHeight(){ - $timeout(function(){ - //total height minus room for avatar and help icon - var height = $(window).height()-200; - scope.totalSections = scope.sections.length; - scope.maxSections = Math.floor(height / 70); - scope.needTray = false; - - if(scope.totalSections > scope.maxSections){ - scope.needTray = true; - scope.overflowingSections = scope.maxSections - scope.totalSections; - } - }); - } - - var evts = []; - - //Listen for global state changes - evts.push(eventsService.on("appState.globalState.changed", function(e, args) { - if (args.key === "showTray") { - scope.showTray = args.value; - } - if (args.key === "stickyNavigation") { - scope.stickyNavigation = args.value; - } - })); - - evts.push(eventsService.on("appState.sectionState.changed", function(e, args) { - if (args.key === "currentSection") { - scope.currentSection = args.value; - } - })); - - evts.push(eventsService.on("app.reInitialize", function(e, args) { - //re-load the sections if we're re-initializing (i.e. package installed) - loadSections(); - })); +**/ + function sectionsDirective($timeout, $window, navigationService, treeService, sectionService, appState, eventsService, $location, historyService) { + return { + restrict: 'E', + // restrict to an element + replace: true, + // replace the html element with the template + templateUrl: 'views/components/application/umb-sections.html', + link: function (scope, element, attr, ctrl) { + //setup scope vars + scope.maxSections = 7; + scope.overflowingSections = 0; + scope.sections = []; + scope.currentSection = appState.getSectionState('currentSection'); + scope.showTray = false; + //appState.getGlobalState("showTray"); + scope.stickyNavigation = appState.getGlobalState('stickyNavigation'); + scope.needTray = false; + scope.trayAnimation = function () { + if (scope.showTray) { + return 'slide'; + } else if (scope.showTray === false) { + return 'slide'; + } else { + return ''; + } + }; + function loadSections() { + sectionService.getSectionsForUser().then(function (result) { + scope.sections = result; + calculateHeight(); + }); + } + function calculateHeight() { + $timeout(function () { + //total height minus room for avatar and help icon + var height = $(window).height() - 200; + scope.totalSections = scope.sections.length; + scope.maxSections = Math.floor(height / 70); + scope.needTray = false; + if (scope.totalSections > scope.maxSections) { + scope.needTray = true; + scope.overflowingSections = scope.maxSections - scope.totalSections; + } + }); + } + var evts = []; + //Listen for global state changes + evts.push(eventsService.on('appState.globalState.changed', function (e, args) { + if (args.key === 'showTray') { + scope.showTray = args.value; + } + if (args.key === 'stickyNavigation') { + scope.stickyNavigation = args.value; + } + })); + evts.push(eventsService.on('appState.sectionState.changed', function (e, args) { + if (args.key === 'currentSection') { + scope.currentSection = args.value; + } + })); + evts.push(eventsService.on('app.reInitialize', function (e, args) { + //re-load the sections if we're re-initializing (i.e. package installed) + loadSections(); + })); + //ensure to unregister from all events! + scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + //on page resize + window.onresize = calculateHeight; + scope.avatarClick = function () { + if (scope.helpDialog) { + closeHelpDialog(); + } + if (!scope.userDialog) { + scope.userDialog = { + view: 'user', + show: true, + close: function (oldModel) { + closeUserDialog(); + } + }; + } else { + closeUserDialog(); + } + }; + function closeUserDialog() { + scope.userDialog.show = false; + scope.userDialog = null; + } + scope.helpClick = function () { + if (scope.userDialog) { + closeUserDialog(); + } + if (!scope.helpDialog) { + scope.helpDialog = { + view: 'help', + show: true, + close: function (oldModel) { + closeHelpDialog(); + } + }; + } else { + closeHelpDialog(); + } + }; + function closeHelpDialog() { + scope.helpDialog.show = false; + scope.helpDialog = null; + } + scope.sectionClick = function (event, section) { + if (event.ctrlKey || event.shiftKey || event.metaKey || event.button && event.button === 1 // middle click, >IE9 + everyone else +) { + return; + } + if (scope.userDialog) { + closeUserDialog(); + } + if (scope.helpDialog) { + closeHelpDialog(); + } + navigationService.hideSearch(); + navigationService.showTree(section.alias); + //in some cases the section will have a custom route path specified, if there is one we'll use it + if (section.routePath) { + $location.path(section.routePath); + } else { + var lastAccessed = historyService.getLastAccessedItemForSection(section.alias); + var path = lastAccessed != null ? lastAccessed.link : section.alias; + $location.path(path).search(''); + } + }; + scope.sectionDblClick = function (section) { + navigationService.reloadSection(section.alias); + }; + scope.trayClick = function () { + // close dialogs + if (scope.userDialog) { + closeUserDialog(); + } + if (scope.helpDialog) { + closeHelpDialog(); + } + if (appState.getGlobalState('showTray') === true) { + navigationService.hideTray(); + } else { + navigationService.showTray(); + } + }; + loadSections(); + } + }; + } + angular.module('umbraco.directives').directive('umbSections', sectionsDirective); + /** +@ngdoc directive +@name umbraco.directives.directive:umbButton +@restrict E +@scope - //ensure to unregister from all events! - scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); +@description +Use this directive to render an umbraco button. The directive can be used to generate all types of buttons, set type, style, translation, shortcut and much more. - //on page resize - window.onresize = calculateHeight; +

    Markup example

    +
    +    
    - scope.avatarClick = function(){ + + - if(scope.helpDialog) { - closeHelpDialog(); - } +
    +
    - if(!scope.userDialog) { - scope.userDialog = { - view: "user", - show: true, - close: function(oldModel) { - closeUserDialog(); - } - }; - } else { - closeUserDialog(); - } +

    Controller example

    +
    +    (function () {
    +        "use strict";
     
    -			};
    +        function Controller(myService) {
     
    -            function closeUserDialog() {
    -                scope.userDialog.show = false;
    -                scope.userDialog = null;
    -            }
    +            var vm = this;
    +            vm.buttonState = "init";
     
    -			scope.helpClick = function(){
    +            vm.clickButton = clickButton;
     
    -                if(scope.userDialog) {
    -                    closeUserDialog();
    -                }
    +            function clickButton() {
     
    -                if(!scope.helpDialog) {
    -                    scope.helpDialog = {
    -                        view: "help",
    -                        show: true,
    -                        close: function(oldModel) {
    -                            closeHelpDialog();
    -                        }
    -                    };
    -                } else {
    -                    closeHelpDialog();
    -                }
    +                vm.buttonState = "busy";
     
    -			};
    +                myService.clickButton().then(function() {
    +                    vm.buttonState = "success";
    +                }, function() {
    +                    vm.buttonState = "error";
    +                });
     
    -            function closeHelpDialog() {
    -                scope.helpDialog.show = false;
    -                scope.helpDialog = null;
                 }
    -
    -			scope.sectionClick = function (event, section) {
    -
    -			    if (event.ctrlKey ||
    -			        event.shiftKey ||
    -			        event.metaKey || // apple
    -			        (event.button && event.button === 1) // middle click, >IE9 + everyone else
    -			    ) {
    -			        return;
    -			    }
    -
    -                if (scope.userDialog) {
    -                    closeUserDialog();
    -			    }
    -			    if (scope.helpDialog) {
    -                    closeHelpDialog();
    -			    }
    -
    -			    navigationService.hideSearch();
    -			    navigationService.showTree(section.alias);
    -			    $location.path("/" + section.alias);
    -			};
    -
    -			scope.sectionDblClick = function(section){
    -				navigationService.reloadSection(section.alias);
    -			};
    -
    -			scope.trayClick = function () {
    -                // close dialogs
    -			    if (scope.userDialog) {
    -                    closeUserDialog();
    -			    }
    -			    if (scope.helpDialog) {
    -                    closeHelpDialog();
    -			    }
    -
    -			    if (appState.getGlobalState("showTray") === true) {
    -			        navigationService.hideTray();
    -			    } else {
    -			        navigationService.showTray();
    -			    }
    -			};
    -
    -			loadSections();
    -
             }
    -    };
    -}
    -
    -angular.module('umbraco.directives').directive("umbSections", sectionsDirective);
    -
    -/**
    -@ngdoc directive
    -@name umbraco.directives.directive:umbButton
    -@restrict E
    -@scope
    -
    -@description
    -Use this directive to render an umbraco button. The directive can be used to generate all types of buttons, set type, style, translation, shortcut and much more.
    -
    -

    Markup example

    -
    -    
    - - - - -
    -
    - -

    Controller example

    -
    -    (function () {
    -        "use strict";
    -
    -        function Controller(myService) {
    -
    -            var vm = this;
    -            vm.buttonState = "init";
    -
    -            vm.clickButton = clickButton;
    -
    -            function clickButton() {
    -
    -                vm.buttonState = "busy";
    -
    -                myService.clickButton().then(function() {
    -                    vm.buttonState = "success";
    -                }, function() {
    -                    vm.buttonState = "error";
    -                });
    -
    -            }
    -        }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -
    -    })();
    -
    - -@param {callback} action The button action which should be performed when the button is clicked. -@param {string=} href Url/Path to navigato to. -@param {string=} type Set the button type ("button" or "submit"). -@param {string=} buttonStyle Set the style of the button. The directive uses the default bootstrap styles ("primary", "info", "success", "warning", "danger", "inverse", "link"). -@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). -@param {string=} shortcut Set a keyboard shortcut for the button ("ctrl+c"). -@param {string=} label Set the button label. -@param {string=} labelKey Set a localization key to make a multi lingual button ("general_buttonText"). -@param {string=} icon Set a button icon. Can only be used when buttonStyle is "link". -@param {boolean=} disabled Set to true to disable the button. + + angular.module("umbraco").controller("My.Controller", Controller); + + })(); +
    + +@param {callback} action The button action which should be performed when the button is clicked. +@param {string=} href Url/Path to navigato to. +@param {string=} type Set the button type ("button" or "submit"). +@param {string=} buttonStyle Set the style of the button. The directive uses the default bootstrap styles ("primary", "info", "success", "warning", "danger", "inverse", "link", "block"). Pass in array to add multple styles [success,block]. +@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). +@param {string=} shortcut Set a keyboard shortcut for the button ("ctrl+c"). +@param {string=} label Set the button label. +@param {string=} labelKey Set a localization key to make a multi lingual button ("general_buttonText"). +@param {string=} icon Set a button icon. +@param {string=} size Set a button icon ("xs", "m", "l", "xl"). +@param {boolean=} disabled Set to true to disable the button. **/ - -(function() { - 'use strict'; - - function ButtonDirective($timeout) { - - function link(scope, el, attr, ctrl) { - - scope.style = null; - - function activate() { - - if (!scope.state) { - scope.state = "init"; - } - - if (scope.buttonStyle) { - scope.style = "btn-" + scope.buttonStyle; - } - - } - - activate(); - - var unbindStateWatcher = scope.$watch('state', function(newValue, oldValue) { - - if (newValue === 'success' || newValue === 'error') { - $timeout(function() { - scope.state = 'init'; - }, 2000); - } - - }); - - scope.$on('$destroy', function() { - unbindStateWatcher(); - }); - - } - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/buttons/umb-button.html', - link: link, - scope: { - action: "&?", - href: "@?", - type: "@", - buttonStyle: "@?", - state: "=?", - shortcut: "@?", - shortcutWhenHidden: "@", - label: "@?", - labelKey: "@?", - icon: "@?", - disabled: "=" - } - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbButton', ButtonDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbButtonGroup -@restrict E -@scope - -@description -Use this directive to render a button with a dropdown of alternative actions. - -

    Markup example

    -
    -    
    - - - - -
    -
    - -

    Controller example

    -
         (function () {
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -
    -            vm.buttonGroup = {
    -                defaultButton: {
    -                    labelKey: "general_defaultButton",
    -                    hotKey: "ctrl+d",
    -                    hotKeyWhenHidden: true,
    -                    handler: function() {
    -                        // do magic here
    +        'use strict';
    +        function ButtonDirective($timeout) {
    +            function link(scope, el, attr, ctrl) {
    +                scope.style = null;
    +                function activate() {
    +                    scope.blockElement = false;
    +                    if (!scope.state) {
    +                        scope.state = 'init';
                         }
    -                },
    -                subButtons: [
    -                    {
    -                        labelKey: "general_subButton",
    -                        hotKey: "ctrl+b",
    -                        hotKeyWhenHidden: true,
    -                        handler: function() {
    -                            // do magic here
    +                    if (scope.buttonStyle) {
    +                        // make it possible to pass in multiple styles
    +                        if (scope.buttonStyle.startsWith('[') && scope.buttonStyle.endsWith(']')) {
    +                            // when using an attr it will always be a string so we need to remove square brackets
    +                            // and turn it into and array
    +                            var withoutBrackets = scope.buttonStyle.replace(/[\[\]']+/g, '');
    +                            // split array by , + make sure to catch whitespaces
    +                            var array = withoutBrackets.split(/\s?,\s?/g);
    +                            angular.forEach(array, function (item) {
    +                                scope.style = scope.style + ' ' + 'btn-' + item;
    +                                if (item === 'block') {
    +                                    scope.blockElement = true;
    +                                }
    +                            });
    +                        } else {
    +                            scope.style = 'btn-' + scope.buttonStyle;
    +                            if (scope.buttonStyle === 'block') {
    +                                scope.blockElement = true;
    +                            }
                             }
                         }
    -                ]
    +                }
    +                activate();
    +                var unbindStateWatcher = scope.$watch('state', function (newValue, oldValue) {
    +                    if (newValue === 'success' || newValue === 'error') {
    +                        $timeout(function () {
    +                            scope.state = 'init';
    +                        }, 2000);
    +                    }
    +                });
    +                scope.$on('$destroy', function () {
    +                    unbindStateWatcher();
    +                });
    +            }
    +            var directive = {
    +                transclude: true,
    +                restrict: 'E',
    +                replace: true,
    +                templateUrl: 'views/components/buttons/umb-button.html',
    +                link: link,
    +                scope: {
    +                    action: '&?',
    +                    href: '@?',
    +                    type: '@',
    +                    buttonStyle: '@?',
    +                    state: '=?',
    +                    shortcut: '@?',
    +                    shortcutWhenHidden: '@',
    +                    label: '@?',
    +                    labelKey: '@?',
    +                    icon: '@?',
    +                    disabled: '=',
    +                    size: '@?'
    +                }
                 };
    +            return directive;
             }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -
    -    })();
    -
    - -

    Button model description

    -
      -
    • - labekKey - (string) - - Set a localization key to make a multi lingual button ("general_buttonText"). -
    • -
    • - hotKey - (array) - - Set a keyboard shortcut for the button ("ctrl+c"). -
    • -
    • - hotKeyWhenHidden - (boolean) - - As a default the hotkeys only works on elements visible in the UI. Set to true to set a hotkey on the hidden sub buttons. -
    • -
    • - handler - (callback) - - Set a callback to handle button click events. -
    • -
    - -@param {object} defaultButton The model of the default button. -@param {array} subButtons Array of sub buttons. -@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). -@param {string=} direction Set the direction of the dropdown ("up", "down"). -@param {string=} float Set the float of the dropdown. ("left", "right"). -**/ - -(function() { - 'use strict'; - - function ButtonGroupDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/buttons/umb-button-group.html', - scope: { - defaultButton: "=", - subButtons: "=", - state: "=?", - direction: "@?", - float: "@?" - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbButtonGroup', ButtonGroupDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorSubHeader -@restrict E - -@description -Use this directive to construct a sub header in the main editor window. -The sub header is sticky and will follow along down the page when scrolling. - -

    Markup example

    -
    -    
    - -
    - - - - - - - // sub header content here - - - - - - -
    - -
    -
    - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
    • -
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
    • -
    • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
    • -
    -**/ - -(function() { - 'use strict'; - - function EditorSubHeaderDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/subheader/umb-editor-sub-header.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubHeader', EditorSubHeaderDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorSubHeaderContentLeft -@restrict E - -@description -Use this directive to left align content in a sub header in the main editor window. - -

    Markup example

    -
    -    
    - -
    - - - - - - - - - // left content here - - - - // right content here - - - - - - - - -
    - -
    -
    - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
    • -
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
    • -
    • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
    • -
    -**/ - -(function() { - 'use strict'; - - function EditorSubHeaderContentLeftDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-left.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubHeaderContentLeft', EditorSubHeaderContentLeftDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorSubHeaderContentRight -@restrict E - -@description -Use this directive to rigt align content in a sub header in the main editor window. - -

    Markup example

    -
    -    
    - -
    - - - - - - - - - // left content here - - - - // right content here - - - - - - - - -
    - -
    -
    - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
    • -
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
    • -
    • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
    • -
    -**/ - -(function() { - 'use strict'; - - function EditorSubHeaderContentRightDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-right.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubHeaderContentRight', EditorSubHeaderContentRightDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorSubHeaderSection -@restrict E - -@description -Use this directive to create sections, divided by borders, in a sub header in the main editor window. - -

    Markup example

    -
    -    
    - -
    - - - - - - - - - - - // section content here - - - - // section content here - - - - // section content here - - - - - - - - - - -
    - -
    -
    - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
    • -
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
    • -
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
    • -
    + angular.module('umbraco.directives').directive('umbButton', ButtonDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbButtonGroup +@restrict E +@scope + +@description +Use this directive to render a button with a dropdown of alternative actions. + +

    Markup example

    +
    +    
    + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +
    +            vm.buttonGroup = {
    +                defaultButton: {
    +                    labelKey: "general_defaultButton",
    +                    hotKey: "ctrl+d",
    +                    hotKeyWhenHidden: true,
    +                    handler: function() {
    +                        // do magic here
    +                    }
    +                },
    +                subButtons: [
    +                    {
    +                        labelKey: "general_subButton",
    +                        hotKey: "ctrl+b",
    +                        hotKeyWhenHidden: true,
    +                        handler: function() {
    +                            // do magic here
    +                        }
    +                    }
    +                ]
    +            };
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +
    +    })();
    +
    + +

    Button model description

    +
      +
    • + labekKey + (string) - + Set a localization key to make a multi lingual button ("general_buttonText"). +
    • +
    • + hotKey + (array) - + Set a keyboard shortcut for the button ("ctrl+c"). +
    • +
    • + hotKeyWhenHidden + (boolean) - + As a default the hotkeys only works on elements visible in the UI. Set to true to set a hotkey on the hidden sub buttons. +
    • +
    • + handler + (callback) - + Set a callback to handle button click events. +
    • +
    + +@param {object} defaultButton The model of the default button. +@param {array} subButtons Array of sub buttons. +@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). +@param {string=} direction Set the direction of the dropdown ("up", "down"). +@param {string=} float Set the float of the dropdown. ("left", "right"). **/ - -(function() { - 'use strict'; - - function EditorSubHeaderSectionDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-section.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubHeaderSection', EditorSubHeaderSectionDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbBreadcrumbs -@restrict E -@scope - -@description -Use this directive to generate a list of breadcrumbs. - -

    Markup example

    -
    -    
    - - -
    -
    - -

    Controller example

    -
         (function () {
    -        "use strict";
    -
    -        function Controller(myService) {
    -
    -            var vm = this;
    -            vm.ancestors = [];
    -
    -            myService.getAncestors().then(function(ancestors){
    -                vm.ancestors = ancestors;
    -            });
    -
    -        }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    - -@param {array} ancestors Array of ancestors -@param {string} entityType The content entity type (member, media, content). -@param {callback} Callback when an ancestor is clicked. It will override the default link behaviour. -**/ - -(function () { - 'use strict'; - - function BreadcrumbsDirective() { - - function link(scope, el, attr, ctrl) { - - scope.allowOnOpen = false; - - scope.open = function(ancestor) { - if(scope.onOpen && scope.allowOnOpen) { - scope.onOpen({'ancestor': ancestor}); + 'use strict'; + function ButtonGroupDirective() { + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/buttons/umb-button-group.html', + scope: { + defaultButton: '=', + subButtons: '=', + state: '=?', + direction: '@?', + float: '@?' } }; - - function onInit() { - if ("onOpen" in attr) { - scope.allowOnOpen = true; - } - } - - onInit(); - + return directive; } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-breadcrumbs.html', - scope: { - ancestors: "=", - entityType: "@", - onOpen: "&" - }, - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorContainer -@restrict E - -@description -Use this directive to construct a main content area inside the main editor window. - -

    Markup example

    -
    -    
    - - - - - - - - // main content here - - - - // footer content here - - - - -
    -
    - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • -
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • -
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • -
    -**/ - -(function() { - 'use strict'; - - function EditorContainerDirective(overlayHelper) { - - function link(scope, el, attr, ctrl) { - - scope.numberOfOverlays = 0; - - scope.$watch(function(){ - return overlayHelper.getNumberOfOverlays(); - }, function (newValue) { - scope.numberOfOverlays = newValue; - }); - - } - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-container.html', - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorContainer', EditorContainerDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorFooter -@restrict E - -@description -Use this directive to construct a footer inside the main editor window. - -

    Markup example

    -
    -    
    - -
    - - - - - - - - // main content here - - - - // footer content here - - - - -
    - -
    -
    - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • -
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • -
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • -
    • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
    • -
    • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
    • -
    -**/ - -(function() { - 'use strict'; - - function EditorFooterDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-footer.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorFooter', EditorFooterDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorFooterContentLeft -@restrict E - -@description -Use this directive to align content left inside the main editor footer. - -

    Markup example

    -
    -    
    - -
    - - - - - - - // align content left - - - - // align content right - - - - - - -
    - -
    -
    - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • -
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • -
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • -
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • -
    • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
    • -
    -**/ - -(function() { - 'use strict'; - - function EditorFooterContentLeftDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-footer-content-left.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorFooterContentLeft', EditorFooterContentLeftDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorFooterContentRight -@restrict E - -@description -Use this directive to align content right inside the main editor footer. - -

    Markup example

    -
    -    
    - -
    - - - - - - - // align content left - - - - // align content right - - - - - - -
    - -
    -
    - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • -
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • -
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • -
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • -
    • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
    • -
    + angular.module('umbraco.directives').directive('umbButtonGroup', ButtonGroupDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbToggle +@restrict E +@scope + +@description +Added in Umbraco version 7.7.0 Use this directive to render an umbraco toggle. + +

    Markup example

    +
    +    
    + + + + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +            vm.checked = false;
    +
    +            vm.toggle = toggle;
    +
    +            function toggle() {
    +                vm.checked = !vm.checked;
    +            }
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +
    +    })();
    +
    + +@param {boolean} checked Set to true or false to toggle the switch. +@param {callback} onClick The function which should be called when the toggle is clicked. +@param {string=} showLabels Set to true or false to show a "On" or "Off" label next to the switch. +@param {string=} labelOn Set a custom label for when the switched is turned on. It will default to "On". +@param {string=} labelOff Set a custom label for when the switched is turned off. It will default to "Off". +@param {string=} labelPosition Sets the label position to the left or right of the switch. It will default to "left" ("left", "right"). +@param {string=} hideIcons Set to true or false to hide the icons on the switch. + **/ - -(function() { - 'use strict'; - - function EditorFooterContentRightDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-footer-content-right.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorFooterContentRight', EditorFooterContentRightDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorHeader -@restrict E -@scope - -@description -Use this directive to construct a header inside the main editor window. - -

    Markup example

    -
    -    
    - -
    - - - - - - - - // main content here - - - - // footer content here - - - - -
    - -
    -
    - -

    Markup example - with tabs

    -
    -    
    - -
    - - - - - - - - - - -
    - // tab 1 content -
    - -
    - // tab 2 content -
    - -
    -
    -
    - - - // footer content here - - -
    - -
    - -
    -
    - -

    Controller example - with tabs

    -
         (function () {
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -            vm.content = {
    -                name: "",
    -                tabs: [
    -                    {
    -                        id: 1,
    -                        label: "Tab 1",
    -                        alias: "tab1",
    -                        active: true
    -                    },
    -                    {
    -                        id: 2,
    -                        label: "Tab 2",
    -                        alias: "tab2",
    -                        active: false
    +        'use strict';
    +        function ToggleDirective(localizationService) {
    +            function link(scope, el, attr, ctrl) {
    +                scope.displayLabelOn = '';
    +                scope.displayLabelOff = '';
    +                function onInit() {
    +                    setLabelText();
    +                }
    +                function setLabelText() {
    +                    // set default label for "on"
    +                    if (scope.labelOn) {
    +                        scope.displayLabelOn = scope.labelOn;
    +                    } else {
    +                        localizationService.localize('general_on').then(function (value) {
    +                            scope.displayLabelOn = value;
    +                        });
                         }
    -                ]
    -            };
    -
    -        }
    -
    -        angular.module("umbraco").controller("MySection.Controller", Controller);
    -    })();
    -
    - -

    Markup example - with sub views

    -
    -    
    - -
    - - - - - - - - - - - - - - - // footer content here - - - - -
    - -
    -
    - -

    Controller example - with sub views

    -
    -    (function () {
    -
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -            vm.content = {
    -                name: "",
    -                navigation: [
    -                    {
    -                        "name": "Section 1",
    -                        "icon": "icon-document-dashed-line",
    -                        "view": "/App_Plugins/path/to/html.html",
    -                        "active": true
    -                    },
    -                    {
    -                        "name": "Section 2",
    -                        "icon": "icon-list",
    -                        "view": "/App_Plugins/path/to/html.html",
    +                    // set default label for "Off"
    +                    if (scope.labelOff) {
    +                        scope.displayLabelOff = scope.labelOff;
    +                    } else {
    +                        localizationService.localize('general_off').then(function (value) {
    +                            scope.displayLabelOff = value;
    +                        });
                         }
    -                ]
    -            };
    -
    -        }
    -
    -        angular.module("umbraco").controller("MySection.Controller", Controller);
    -    })();
    -
    - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • -
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • -
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • -
    - -@param {string} name The content name. -@param {array=} tabs Array of tabs. See example above. -@param {array=} navigation Array of sub views. See example above. -@param {boolean=} nameLocked Set to true to lock the name. -@param {object=} menu Add a context menu to the editor. -@param {string=} icon Show and edit the content icon. Opens an overlay to change the icon. -@param {boolean=} hideIcon Set to true to hide icon. -@param {string=} alias show and edit the content alias. -@param {boolean=} hideAlias Set to true to hide alias. -@param {string=} description Add a description to the content. -@param {boolean=} hideDescription Set to true to hide description. - -**/ - -(function() { - 'use strict'; - - function EditorHeaderDirective(iconHelper) { - - function link(scope, el, attr, ctrl) { - - scope.openIconPicker = function() { - scope.dialogModel = { - view: "iconpicker", - show: true, - submit: function (model) { - - /* ensure an icon is selected, because on focus on close button - or an element in background no icon is submitted. So don't clear/update existing icon/preview. - */ - if (model.icon) { - - if (model.color) { - scope.icon = model.icon + " " + model.color; - } else { - scope.icon = model.icon; - } - - // set form to dirty - ctrl.$setDirty(); - } - - scope.dialogModel.show = false; - scope.dialogModel = null; + } + scope.click = function () { + if (scope.onClick) { + scope.onClick(); } }; + onInit(); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/buttons/umb-toggle.html', + scope: { + checked: '=', + onClick: '&', + labelOn: '@?', + labelOff: '@?', + labelPosition: '@?', + showLabels: '@?', + hideIcons: '@?' + }, + link: link }; + return directive; } - - var directive = { - require: '^form', - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-header.html', - scope: { - tabs: "=", - actions: "=", - name: "=", - nameLocked: "=", - menu: "=", - icon: "=", - hideIcon: "@", - alias: "=", - hideAlias: "@", - description: "=", - hideDescription: "@", - descriptionLocked: "@", - navigation: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorHeader', EditorHeaderDirective); - -})(); - -(function() { - 'use strict'; - - function EditorMenuDirective($injector, treeService, navigationService, umbModelMapper, appState) { - - function link(scope, el, attr, ctrl) { - - //adds a handler to the context menu item click, we need to handle this differently - //depending on what the menu item is supposed to do. - scope.executeMenuItem = function (action) { - navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); - }; - - //callback method to go and get the options async - scope.getOptions = function () { - - if (!scope.currentNode) { - return; - } - - //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) - appState.setMenuState("currentNode", scope.currentNode); - - if (!scope.actions) { - treeService.getMenu({ treeNode: scope.currentNode }) - .then(function (data) { - scope.actions = data.menuItems; - }); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-menu.html', - link: link, - scope: { - currentNode: "=", - currentSection: "@" - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorMenu', EditorMenuDirective); - -})(); - -(function() { - 'use strict'; - - function EditorNavigationDirective() { - - function link(scope, el, attr, ctrl) { - - scope.showNavigation = true; - - scope.clickNavigationItem = function(selectedItem) { - setItemToActive(selectedItem); - runItemAction(selectedItem); - }; - - function runItemAction(selectedItem) { - if (selectedItem.action) { - selectedItem.action(selectedItem); - } - } - - function setItemToActive(selectedItem) { - // set all other views to inactive - if (selectedItem.view) { - - for (var index = 0; index < scope.navigation.length; index++) { - var item = scope.navigation[index]; - item.active = false; - } - - // set view to active - selectedItem.active = true; - - } - } - - function activate() { - - // hide navigation if there is only 1 item - if (scope.navigation.length <= 1) { - scope.showNavigation = false; + angular.module('umbraco.directives').directive('umbToggle', ToggleDirective); + }()); + (function () { + 'use strict'; + function ContentEditController($rootScope, $scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { + //setup scope vars + $scope.defaultButton = null; + $scope.subButtons = []; + $scope.page = {}; + $scope.page.loading = false; + $scope.page.menu = {}; + $scope.page.menu.currentNode = null; + $scope.page.menu.currentSection = appState.getSectionState('currentSection'); + $scope.page.listViewPath = null; + $scope.page.isNew = $scope.isNew ? true : false; + $scope.page.buttonGroupState = 'init'; + function init(content) { + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $scope.page.isNew, + content: content, + methods: { + saveAndPublish: $scope.saveAndPublish, + sendToPublish: $scope.sendToPublish, + save: $scope.save, + unPublish: $scope.unPublish + } + }); + $scope.defaultButton = buttons.defaultButton; + $scope.subButtons = buttons.subButtons; + editorState.set($scope.content); + //We fetch all ancestors of the node to generate the footer breadcrumb navigation + if (!$scope.page.isNew) { + if (content.parentId && content.parentId !== -1) { + entityResource.getAncestors(content.id, 'document').then(function (anc) { + $scope.ancestors = anc; + }); + } + } } - - } - - activate(); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-navigation.html', - scope: { - navigation: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives.html').directive('umbEditorNavigation', EditorNavigationDirective); - -})(); - -(function() { - 'use strict'; - - function EditorSubViewsDirective() { - - function link(scope, el, attr, ctrl) { - - scope.activeView = {}; - - // set toolbar from selected navigation item - function setActiveView(items) { - - for (var index = 0; index < items.length; index++) { - - var item = items[index]; - - if (item.active && item.view) { - scope.activeView = item; - } + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(content, path, initialLoad) { + if (!$scope.content.isChildOfListView) { + navigationService.syncTree({ + tree: $scope.treeAlias, + path: path.split(','), + forceReload: initialLoad !== true + }).then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }); + } else if (initialLoad === true) { + //it's a child item, just sync the ui node to the parent + navigationService.syncTree({ + tree: $scope.treeAlias, + path: path.substring(0, path.lastIndexOf(',')).split(','), + forceReload: initialLoad !== true + }); + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + // from the server so that we can load in the actions menu. + umbRequestHelper.resourcePromise($http.get(content.treeNodeUrl), 'Failed to retrieve data for child node ' + content.id).then(function (node) { + $scope.page.menu.currentNode = node; + }); + } } - } - - // watch for navigation changes - scope.$watch('subViews', function(newValue, oldValue) { - if (newValue) { - setActiveView(newValue); + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish + function performSave(args) { + var deferred = $q.defer(); + $scope.page.buttonGroupState = 'busy'; + contentEditingHelper.contentEditorPerformSave({ + statusMessage: args.statusMessage, + saveMethod: args.saveMethod, + scope: $scope, + content: $scope.content, + action: args.action + }).then(function (data) { + //success + init($scope.content); + syncTreeNode($scope.content, data.path); + $scope.page.buttonGroupState = 'success'; + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + $scope.page.buttonGroupState = 'error'; + deferred.reject(err); + }); + return deferred.promise; + } + function resetLastListPageNumber(content) { + // We're using rootScope to store the page number for list views, so if returning to the list + // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children + // we should remove this so as not to confuse if navigating to a different list + if (!content.isChildOfListView && !content.isContainer) { + $rootScope.lastListViewPageViewed = null; + } + } + if ($scope.page.isNew) { + $scope.page.loading = true; + //we are creating so get an empty content item + $scope.getScaffoldMethod()().then(function (data) { + $scope.content = data; + init($scope.content); + resetLastListPageNumber($scope.content); + $scope.page.loading = false; + }); + } else { + $scope.page.loading = true; + //we are editing so get the content item from the server + $scope.getMethod()($scope.contentId).then(function (data) { + $scope.content = data; + if (data.isChildOfListView && data.trashed === false) { + $scope.page.listViewPath = $routeParams.page ? '/content/content/edit/' + data.parentId + '?page=' + $routeParams.page : '/content/content/edit/' + data.parentId; + } + init($scope.content); + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + syncTreeNode($scope.content, data.path, true); + resetLastListPageNumber($scope.content); + $scope.page.loading = false; + }); } - }, true); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-sub-views.html', - scope: { - subViews: "=", - model: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorView -@restrict E -@scope - -@description -Use this directive to construct the main editor window. - -

    Markup example

    -
    -    
    - -
    - - - - - - - - // main content here - - - - // footer content here - - - - -
    - -
    -
    -

    Controller example

    -
    -    (function () {
    -
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -
    -        }
    -
    -        angular.module("umbraco").controller("MySection.Controller", Controller);
    -    })();
    -
    - - -

    Use in combination with

    -
      -
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • -
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • -
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • -
    -**/ - -(function() { - 'use strict'; - - function EditorViewDirective() { - - function link(scope, el, attr) { - - if(attr.footer) { - scope.footer = attr.footer; - } - - } - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-view.html', - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorView', EditorViewDirective); - -})(); - -/** -* @description Utillity directives for key and field events -**/ -angular.module('umbraco.directives') - -.directive('onKeyup', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onKeyup); + $scope.unPublish = function () { + if (formHelper.submitForm({ + scope: $scope, + statusMessage: 'Unpublishing...', + skipValidation: true + })) { + $scope.page.buttonGroupState = 'busy'; + contentResource.unPublish($scope.content.id).then(function (data) { + formHelper.resetForm({ + scope: $scope, + notifications: data.notifications + }); + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + init($scope.content); + syncTreeNode($scope.content, data.path); + $scope.page.buttonGroupState = 'success'; + }); + } }; - elm.on("keyup", f); - scope.$on("$destroy", function(){ elm.off("keyup", f);} ); - } - }; -}) - -.directive('onKeydown', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onKeydown); + $scope.sendToPublish = function () { + return performSave({ + saveMethod: contentResource.sendToPublish, + statusMessage: 'Sending...', + action: 'sendToPublish' + }); }; - elm.on("keydown", f); - scope.$on("$destroy", function(){ elm.off("keydown", f);} ); - } - }; -}) - -.directive('onBlur', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onBlur); + $scope.saveAndPublish = function () { + return performSave({ + saveMethod: contentResource.publish, + statusMessage: 'Publishing...', + action: 'publish' + }); }; - elm.on("blur", f); - scope.$on("$destroy", function(){ elm.off("blur", f);} ); - } - }; -}) - -.directive('onFocus', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onFocus); + $scope.save = function () { + return performSave({ + saveMethod: $scope.saveMethod(), + statusMessage: 'Saving...', + action: 'save' + }); }; - elm.on("focus", f); - scope.$on("$destroy", function(){ elm.off("focus", f);} ); - } - }; -}) - -.directive('onDragEnter', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragEnter); + $scope.preview = function (content) { + if (!$scope.busy) { + // Chromes popup blocker will kick in if a window is opened + // without the initial scoped request. This trick will fix that. + // + var previewWindow = $window.open('preview/?init=true&id=' + content.id, 'umbpreview'); + // Build the correct path so both /#/ and #/ work. + var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + content.id; + //The user cannot save if they don't have access to do that, in which case we just want to preview + //and that's it otherwise they'll get an unauthorized access message + if (!_.contains(content.allowedActions, 'A')) { + previewWindow.location.href = redirect; + } else { + $scope.save().then(function (data) { + previewWindow.location.href = redirect; + }); + } + } }; - elm.on("dragenter", f); - scope.$on("$destroy", function(){ elm.off("dragenter", f);} ); } - }; -}) - -.directive('onDragLeave', function () { - return function (scope, elm, attrs) { - var f = function (event) { - var rect = this.getBoundingClientRect(); - var getXY = function getCursorPosition(event) { - var x, y; - - if (typeof event.clientX === 'undefined') { - // try touch screen - x = event.pageX + document.documentElement.scrollLeft; - y = event.pageY + document.documentElement.scrollTop; - } else { - x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; + function createDirective() { + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/content/edit.html', + controller: 'Umbraco.Editors.Content.EditorDirectiveController', + scope: { + contentId: '=', + isNew: '=?', + treeAlias: '@', + page: '=?', + saveMethod: '&', + getMethod: '&', + getScaffoldMethod: '&?' } - - return { x: x, y : y }; - }; - - var e = getXY(event.originalEvent); - - // Check the mouseEvent coordinates are outside of the rectangle - if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) { - scope.$apply(attrs.onDragLeave); - } - }; - - elm.on("dragleave", f); - scope.$on("$destroy", function(){ elm.off("dragleave", f);} ); - }; -}) - -.directive('onDragOver', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragOver); }; - elm.on("dragover", f); - scope.$on("$destroy", function(){ elm.off("dragover", f);} ); + return directive; } - }; -}) - -.directive('onDragStart', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragStart); + angular.module('umbraco.directives').controller('Umbraco.Editors.Content.EditorDirectiveController', ContentEditController); + angular.module('umbraco.directives').directive('contentEditor', createDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeader +@restrict E + +@description +Use this directive to construct a sub header in the main editor window. +The sub header is sticky and will follow along down the page when scrolling. + +

    Markup example

    +
    +    
    + +
    + + + + + + + // sub header content here + + + + + + +
    + +
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
    • +
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
    • +
    • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
    • +
    +**/ + (function () { + 'use strict'; + function EditorSubHeaderDirective() { + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header.html' }; - elm.on("dragstart", f); - scope.$on("$destroy", function(){ elm.off("dragstart", f);} ); + return directive; } - }; -}) - -.directive('onDragEnd', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragEnd); + angular.module('umbraco.directives').directive('umbEditorSubHeader', EditorSubHeaderDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeaderContentLeft +@restrict E + +@description +Use this directive to left align content in a sub header in the main editor window. + +

    Markup example

    +
    +    
    + +
    + + + + + + + + + // left content here + + + + // right content here + + + + + + + + +
    + +
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
    • +
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
    • +
    • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
    • +
    +**/ + (function () { + 'use strict'; + function EditorSubHeaderContentLeftDirective() { + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-left.html' }; - elm.on("dragend", f); - scope.$on("$destroy", function(){ elm.off("dragend", f);} ); + return directive; } - }; -}) - -.directive('onDrop', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDrop); + angular.module('umbraco.directives').directive('umbEditorSubHeaderContentLeft', EditorSubHeaderContentLeftDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeaderContentRight +@restrict E + +@description +Use this directive to rigt align content in a sub header in the main editor window. + +

    Markup example

    +
    +    
    + +
    + + + + + + + + + // left content here + + + + // right content here + + + + + + + + +
    + +
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
    • +
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
    • +
    • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
    • +
    +**/ + (function () { + 'use strict'; + function EditorSubHeaderContentRightDirective() { + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-right.html' }; - elm.on("drop", f); - scope.$on("$destroy", function(){ elm.off("drop", f);} ); - } - }; -}) - -.directive('onOutsideClick', function ($timeout) { - return function (scope, element, attrs) { - - var eventBindings = []; - - function oneTimeClick(event) { - var el = event.target.nodeName; - - //ignore link and button clicks - var els = ["INPUT","A","BUTTON"]; - if(els.indexOf(el) >= 0){return;} - - // ignore children of links and buttons - // ignore clicks on new overlay - var parents = $(event.target).parents("a,button,.umb-overlay"); - if(parents.length > 0){ - return; - } - - // ignore clicks on dialog from old dialog service - var oldDialog = $(event.target).parents("#old-dialog-service"); - if (oldDialog.length === 1) { - return; - } - - // ignore clicks in tinyMCE dropdown(floatpanel) - var floatpanel = $(event.target).closest(".mce-floatpanel"); - if (floatpanel.length === 1) { - return; - } - - //ignore clicks inside this element - if( $(element).has( $(event.target) ).length > 0 ){ - return; - } - - scope.$apply(attrs.onOutsideClick); + return directive; } - - - $timeout(function(){ - - if ("bindClickOn" in attrs) { - - eventBindings.push(scope.$watch(function() { - return attrs.bindClickOn; - }, function(newValue) { - if (newValue === "true") { - $(document).on("click", oneTimeClick); - } else { - $(document).off("click", oneTimeClick); - } - })); - - } else { - $(document).on("click", oneTimeClick); - } - - scope.$on("$destroy", function() { - $(document).off("click", oneTimeClick); - - // unbind watchers - for (var e in eventBindings) { - eventBindings[e](); - } - - }); - }); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution. - - }; -}) - -.directive('onRightClick',function(){ - - document.oncontextmenu = function (e) { - if(e.target.hasAttribute('on-right-click')) { - e.preventDefault(); - e.stopPropagation(); - return false; - } - }; - - return function(scope,el,attrs){ - el.on('contextmenu',function(e){ - e.preventDefault(); - e.stopPropagation(); - scope.$apply(attrs.onRightClick); - return false; - }); - }; -}) - -.directive('onDelayedMouseleave', function ($timeout, $parse) { - return { - - restrict: 'A', - - link: function (scope, element, attrs, ctrl) { - var active = false; - var fn = $parse(attrs.onDelayedMouseleave); - - var leave_f = function(event) { - var callback = function() { - fn(scope, {$event:event}); - }; - - active = false; - $timeout(function(){ - if(active === false){ - scope.$apply(callback); - } - }, 650); - }; - - var enter_f = function(event, args){ - active = true; - }; - - - element.on("mouseleave", leave_f); - element.on("mouseenter", enter_f); - - //unsub events - scope.$on("$destroy", function(){ - element.off("mouseleave", leave_f); - element.off("mouseenter", enter_f); - }); - } - }; - }); + angular.module('umbraco.directives').directive('umbEditorSubHeaderContentRight', EditorSubHeaderContentRightDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeaderSection +@restrict E -/* - - http://vitalets.github.io/checklist-model/ - -*/ -angular.module('umbraco.directives') -.directive('checklistModel', ['$parse', '$compile', function($parse, $compile) { - // contains - function contains(arr, item) { - if (angular.isArray(arr)) { - for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { - return true; - } - } - } - return false; - } - - // add - function add(arr, item) { - arr = angular.isArray(arr) ? arr : []; - for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { - return arr; - } - } - arr.push(item); - return arr; - } - - // remove - function remove(arr, item) { - if (angular.isArray(arr)) { - for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { - arr.splice(i, 1); - break; - } - } - } - return arr; - } - - // http://stackoverflow.com/a/19228302/1458162 - function postLinkFn(scope, elem, attrs) { - // compile with `ng-model` pointing to `checked` - $compile(elem)(scope); - - // getter / setter for original model - var getter = $parse(attrs.checklistModel); - var setter = getter.assign; - - // value added to list - var value = $parse(attrs.checklistValue)(scope.$parent); - - // watch UI checked change - scope.$watch('checked', function(newValue, oldValue) { - if (newValue === oldValue) { - return; - } - var current = getter(scope.$parent); - if (newValue === true) { - setter(scope.$parent, add(current, value)); - } else { - setter(scope.$parent, remove(current, value)); - } - }); - - // watch original model change - scope.$parent.$watch(attrs.checklistModel, function(newArr, oldArr) { - scope.checked = contains(newArr, value); - }, true); - } - - return { - restrict: 'A', - priority: 1000, - terminal: true, - scope: true, - compile: function(tElement, tAttrs) { - if (tElement[0].tagName !== 'INPUT' || !tElement.attr('type', 'checkbox')) { - throw 'checklist-model should be applied to `input[type="checkbox"]`.'; - } - - if (!tAttrs.checklistValue) { - throw 'You should provide `checklist-value`.'; - } - - // exclude recursion - tElement.removeAttr('checklist-model'); - - // local scope var storing individual checkbox model - tElement.attr('ng-model', 'checked'); - - return postLinkFn; - } - }; -}]); -angular.module("umbraco.directives") -.directive("contenteditable", function() { - - return { - require: "ngModel", - link: function(scope, element, attrs, ngModel) { - - function read() { - ngModel.$setViewValue(element.html()); - } - - ngModel.$render = function() { - element.html(ngModel.$viewValue || ""); - }; - - - element.bind("focus", function(){ - - var range = document.createRange(); - range.selectNodeContents(element[0]); - - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - - }); - - element.bind("blur keyup change", function() { - scope.$apply(read); - }); - } - - }; - -}); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:fixNumber -* @restrict A -* @description Used in conjunction with type='number' input fields to ensure that the bound value is converted to a number when using ng-model -* because normally it thinks it's a string and also validation doesn't work correctly due to an angular bug. +@description +Use this directive to create sections, divided by borders, in a sub header in the main editor window. + +

    Markup example

    +
    +    
    + +
    + + + + + + + + + + + // section content here + + + + // section content here + + + + // section content here + + + + + + + + + + +
    + +
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
    • +
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
    • +
    • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
    • +
    **/ -function fixNumber($parse) { - return { - restrict: "A", - require: "ngModel", - - link: function (scope, elem, attrs, ctrl) { - - //parse ngModel onload - var modelVal = scope.$eval(attrs.ngModel); - if (modelVal) { - var asNum = parseFloat(modelVal, 10); - if (!isNaN(asNum)) { - $parse(attrs.ngModel).assign(scope, asNum); - } - } - - //always return an int to the model - ctrl.$parsers.push(function (value) { - if (value === 0) { - return 0; - } - return parseFloat(value || '', 10); - }); - - //always try to format the model value as an int - ctrl.$formatters.push(function (value) { - if (angular.isString(value)) { - return parseFloat(value, 10); - } - return value; - }); - - //This fixes this angular issue: - //https://github.com/angular/angular.js/issues/2144 - // which doesn't actually validate the number input properly since the model only changes when a real number is entered - // but the input box still allows non-numbers to be entered which do not validate (only via html5) - if (typeof elem.prop('validity') === 'undefined') { - return; - } - - elem.bind('input', function (e) { - var validity = elem.prop('validity'); - scope.$apply(function () { - ctrl.$setValidity('number', !validity.badInput); - }); - }); - } - }; -} -angular.module('umbraco.directives').directive("fixNumber", fixNumber); -angular.module("umbraco.directives").directive('focusWhen', function ($timeout) { - return { - restrict: 'A', - link: function (scope, elm, attrs, ctrl) { - attrs.$observe("focusWhen", function (newValue) { - if (newValue === "true") { - $timeout(function () { - elm.focus(); - }); - } - }); + (function () { + 'use strict'; + function EditorSubHeaderSectionDirective() { + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-section.html' + }; + return directive; } - }; -}); - - -/** -* @ngdoc directive -* @name umbraco.directives.directive:hexBgColor -* @restrict A -* @description Used to set a hex background color on an element, this will detect valid hex and when it is valid it will set the color, otherwise -* a color will not be set. + angular.module('umbraco.directives').directive('umbEditorSubHeaderSection', EditorSubHeaderSectionDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbBreadcrumbs +@restrict E +@scope + +@description +Use this directive to generate a list of breadcrumbs. + +

    Markup example

    +
    +    
    + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller(myService) {
    +
    +            var vm = this;
    +            vm.ancestors = [];
    +
    +            myService.getAncestors().then(function(ancestors){
    +                vm.ancestors = ancestors;
    +            });
    +
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +    })();
    +
    + +@param {array} ancestors Array of ancestors +@param {string} entityType The content entity type (member, media, content). +@param {callback} Callback when an ancestor is clicked. It will override the default link behaviour. **/ -function hexBgColor() { - return { - restrict: "A", - link: function (scope, element, attr, formCtrl) { - - var origColor = null; - if (attr.hexBgOrig) { - //set the orig based on the attribute if there is one - origColor = attr.hexBgOrig; - } - - attr.$observe("hexBgColor", function (newVal) { - if (newVal) { - if (!origColor) { - //get the orig color before changing it - origColor = element.css("border-color"); - } - //validate it - test with and without the leading hash. - if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { - element.css("background-color", "#" + newVal); - return; + (function () { + 'use strict'; + function BreadcrumbsDirective() { + function link(scope, el, attr, ctrl) { + scope.allowOnOpen = false; + scope.open = function (ancestor) { + if (scope.onOpen && scope.allowOnOpen) { + scope.onOpen({ 'ancestor': ancestor }); } - if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { - element.css("background-color", newVal); - return; + }; + function onInit() { + if ('onOpen' in attr) { + scope.allowOnOpen = true; } } - element.css("background-color", origColor); - }); - + onInit(); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-breadcrumbs.html', + scope: { + ancestors: '=', + entityType: '@', + onOpen: '&' + }, + link: link + }; + return directive; } - }; -} -angular.module('umbraco.directives').directive("hexBgColor", hexBgColor); -/** -* @ngdoc directive -* @name umbraco.directives.directive:hotkey + angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorContainer +@restrict E + +@description +Use this directive to construct a main content area inside the main editor window. + +

    Markup example

    +
    +    
    + + + + + + + + // main content here + + + + // footer content here + + + + +
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • +
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • +
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • +
    **/ - -angular.module("umbraco.directives") - .directive('hotkey', function($window, keyboardService, $log) { - - return function(scope, el, attrs) { - - var options = {}; - var keyCombo = attrs.hotkey; - - if (!keyCombo) { - //support data binding - keyCombo = scope.$eval(attrs["hotkey"]); + (function () { + 'use strict'; + function EditorContainerDirective(overlayHelper) { + function link(scope, el, attr, ctrl) { + scope.numberOfOverlays = 0; + scope.$watch(function () { + return overlayHelper.getNumberOfOverlays(); + }, function (newValue) { + scope.numberOfOverlays = newValue; + }); } - - function activate() { - - if (keyCombo) { - - // disable shortcuts in input fields if keycombo is 1 character - if (keyCombo.length === 1) { - options = { - inputDisabled: true - }; - } - - keyboardService.bind(keyCombo, function() { - - var element = $(el); - var activeElementType = document.activeElement.tagName; - var clickableElements = ["A", "BUTTON"]; - - if (element.is("a,div,button,input[type='button'],input[type='submit'],input[type='checkbox']") && !element.is(':disabled')) { - - if (element.is(':visible') || attrs.hotkeyWhenHidden) { - - if (attrs.hotkeyWhen && attrs.hotkeyWhen === "false") { - return; - } - - // when keycombo is enter and a link or button has focus - click the link or button instead of using the hotkey - if (keyCombo === "enter" && clickableElements.indexOf(activeElementType) === 0) { - document.activeElement.click(); + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-container.html', + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbEditorContainer', EditorContainerDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorFooter +@restrict E + +@description +Use this directive to construct a footer inside the main editor window. + +

    Markup example

    +
    +    
    + +
    + + + + + + + + // main content here + + + + // footer content here + + + + +
    + +
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • +
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • +
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • +
    • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
    • +
    • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
    • +
    +**/ + (function () { + 'use strict'; + function EditorFooterDirective() { + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-footer.html' + }; + return directive; + } + angular.module('umbraco.directives').directive('umbEditorFooter', EditorFooterDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorFooterContentLeft +@restrict E + +@description +Use this directive to align content left inside the main editor footer. + +

    Markup example

    +
    +    
    + +
    + + + + + + + // align content left + + + + // align content right + + + + + + +
    + +
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • +
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • +
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • +
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • +
    • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
    • +
    +**/ + (function () { + 'use strict'; + function EditorFooterContentLeftDirective() { + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-footer-content-left.html' + }; + return directive; + } + angular.module('umbraco.directives').directive('umbEditorFooterContentLeft', EditorFooterContentLeftDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorFooterContentRight +@restrict E + +@description +Use this directive to align content right inside the main editor footer. + +

    Markup example

    +
    +    
    + +
    + + + + + + + // align content left + + + + // align content right + + + + + + +
    + +
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • +
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • +
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • +
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • +
    • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
    • +
    +**/ + (function () { + 'use strict'; + function EditorFooterContentRightDirective() { + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-footer-content-right.html' + }; + return directive; + } + angular.module('umbraco.directives').directive('umbEditorFooterContentRight', EditorFooterContentRightDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorHeader +@restrict E +@scope + +@description +Use this directive to construct a header inside the main editor window. + +

    Markup example

    +
    +    
    + +
    + + + + + + + + // main content here + + + + // footer content here + + + + +
    + +
    +
    + +

    Markup example - with tabs

    +
    +    
    + +
    + + + + + + + + + + +
    + // tab 1 content +
    + +
    + // tab 2 content +
    + +
    +
    +
    + + + // footer content here + + +
    + +
    + +
    +
    + +

    Controller example - with tabs

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +            vm.content = {
    +                name: "",
    +                tabs: [
    +                    {
    +                        id: 1,
    +                        label: "Tab 1",
    +                        alias: "tab1",
    +                        active: true
    +                    },
    +                    {
    +                        id: 2,
    +                        label: "Tab 2",
    +                        alias: "tab2",
    +                        active: false
    +                    }
    +                ]
    +            };
    +
    +        }
    +
    +        angular.module("umbraco").controller("MySection.Controller", Controller);
    +    })();
    +
    + +

    Markup example - with sub views

    +
    +    
    + +
    + + + + + + + + + + + + + + + // footer content here + + + + +
    + +
    +
    + +

    Controller example - with sub views

    +
    +    (function () {
    +
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +            vm.content = {
    +                name: "",
    +                navigation: [
    +                    {
    +                        "name": "Section 1",
    +                        "icon": "icon-document-dashed-line",
    +                        "view": "/App_Plugins/path/to/html.html",
    +                        "active": true
    +                    },
    +                    {
    +                        "name": "Section 2",
    +                        "icon": "icon-list",
    +                        "view": "/App_Plugins/path/to/html.html",
    +                    }
    +                ]
    +            };
    +
    +        }
    +
    +        angular.module("umbraco").controller("MySection.Controller", Controller);
    +    })();
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorView umbEditorView}
    • +
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • +
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • +
    + +@param {string} name The content name. +@param {array=} tabs Array of tabs. See example above. +@param {array=} navigation Array of sub views. See example above. +@param {boolean=} nameLocked Set to true to lock the name. +@param {object=} menu Add a context menu to the editor. +@param {string=} icon Show and edit the content icon. Opens an overlay to change the icon. +@param {boolean=} hideIcon Set to true to hide icon. +@param {string=} alias show and edit the content alias. +@param {boolean=} hideAlias Set to true to hide alias. +@param {string=} description Add a description to the content. +@param {boolean=} hideDescription Set to true to hide description. + +**/ + (function () { + 'use strict'; + function EditorHeaderDirective(iconHelper) { + function link(scope, el, attr, ctrl) { + scope.openIconPicker = function () { + scope.dialogModel = { + view: 'iconpicker', + show: true, + submit: function (model) { + /* ensure an icon is selected, because on focus on close button + or an element in background no icon is submitted. So don't clear/update existing icon/preview. + */ + if (model.icon) { + if (model.color) { + scope.icon = model.icon + ' ' + model.color; } else { - element.click(); + scope.icon = model.icon; } - + // set the icon form to dirty + scope.iconForm.$setDirty(); } - - } else { - element.focus(); + scope.dialogModel.show = false; + scope.dialogModel = null; } - - }, options); - - el.on('$destroy', function() { - keyboardService.unbind(keyCombo); - }); - - } - + }; + }; } - - activate(); - - }; - }); - -/** -@ngdoc directive -@name umbraco.directives.directive:preventDefault - -@description -Use this directive to prevent default action of an element. Effectively implementing jQuery's preventdefault - -

    Markup example

    - -
    -    Don't go to Umbraco.com
    -
    - -**/ -angular.module("umbraco.directives") - .directive('preventDefault', function() { - return function(scope, element, attrs) { - - var enabled = true; - //check if there's a value for the attribute, if there is and it's false then we conditionally don't - //prevent default. - if (attrs.preventDefault) { - attrs.$observe("preventDefault", function (newVal) { - enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; - }); + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-header.html', + scope: { + tabs: '=', + actions: '=', + name: '=', + nameLocked: '=', + menu: '=', + icon: '=', + hideIcon: '@', + alias: '=', + hideAlias: '@', + description: '=', + hideDescription: '@', + descriptionLocked: '@', + navigation: '=', + key: '=' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbEditorHeader', EditorHeaderDirective); + }()); + (function () { + 'use strict'; + function EditorMenuDirective($injector, treeService, navigationService, umbModelMapper, appState) { + function link(scope, el, attr, ctrl) { + //adds a handler to the context menu item click, we need to handle this differently + //depending on what the menu item is supposed to do. + scope.executeMenuItem = function (action) { + navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); + }; + //callback method to go and get the options async + scope.getOptions = function () { + if (!scope.currentNode) { + return; + } + //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) + appState.setMenuState('currentNode', scope.currentNode); + if (!scope.actions) { + treeService.getMenu({ treeNode: scope.currentNode }).then(function (data) { + scope.actions = data.menuItems; + }); + } + }; } - - $(element).click(function (event) { - if (event.metaKey || event.ctrlKey) { - return; + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-menu.html', + link: link, + scope: { + currentNode: '=', + currentSection: '@' } - else { - if (enabled === true) { - event.preventDefault(); + }; + return directive; + } + angular.module('umbraco.directives').directive('umbEditorMenu', EditorMenuDirective); + }()); + (function () { + 'use strict'; + function EditorNavigationDirective() { + function link(scope, el, attr, ctrl) { + scope.showNavigation = true; + scope.clickNavigationItem = function (selectedItem) { + setItemToActive(selectedItem); + runItemAction(selectedItem); + }; + function runItemAction(selectedItem) { + if (selectedItem.action) { + selectedItem.action(selectedItem); } } - }); - }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:preventEnterSubmit -* @description prevents a form from submitting when the enter key is pressed on an input field -**/ -angular.module("umbraco.directives") - .directive('preventEnterSubmit', function() { - return function(scope, element, attrs) { - - var enabled = true; - //check if there's a value for the attribute, if there is and it's false then we conditionally don't - //prevent default. - if (attrs.preventEnterSubmit) { - attrs.$observe("preventEnterSubmit", function (newVal) { - enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; - }); + function setItemToActive(selectedItem) { + // set all other views to inactive + if (selectedItem.view) { + for (var index = 0; index < scope.navigation.length; index++) { + var item = scope.navigation[index]; + item.active = false; + } + // set view to active + selectedItem.active = true; + } + } + function activate() { + // hide navigation if there is only 1 item + if (scope.navigation.length <= 1) { + scope.showNavigation = false; + } + } + activate(); } - - $(element).keypress(function (event) { - if (event.which === 13) { - event.preventDefault(); + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-navigation.html', + scope: { navigation: '=' }, + link: link + }; + return directive; + } + angular.module('umbraco.directives.html').directive('umbEditorNavigation', EditorNavigationDirective); + }()); + (function () { + 'use strict'; + function EditorSubViewsDirective() { + function link(scope, el, attr, ctrl) { + scope.activeView = {}; + // set toolbar from selected navigation item + function setActiveView(items) { + for (var index = 0; index < items.length; index++) { + var item = items[index]; + if (item.active && item.view) { + scope.activeView = item; + } + } } - }); - }; - }); -/** - * @ngdoc directive - * @name umbraco.directives.directive:resizeToContent - * @element div - * @function - * - * @description - * Resize iframe's automatically to fit to the content they contain - * - * @example - - - - - - */ -angular.module("umbraco.directives") - .directive('resizeToContent', function ($window, $timeout) { - return function (scope, el, attrs) { - var iframe = el[0]; - var iframeWin = iframe.contentWindow || iframe.contentDocument.parentWindow; - if (iframeWin.document.body) { - - $timeout(function(){ - var height = iframeWin.document.documentElement.scrollHeight || iframeWin.document.body.scrollHeight; - el.height(height); - }, 3000); - } - }; - }); - -angular.module("umbraco.directives") - .directive('selectOnFocus', function () { - return function (scope, el, attrs) { - $(el).bind("click", function () { - var editmode = $(el).data("editmode"); - //If editmode is true a click is handled like a normal click - if (!editmode) { - //Initial click, select entire text - this.select(); - //Set the edit mode so subsequent clicks work normally - $(el).data("editmode", true); + // watch for navigation changes + scope.$watch('subViews', function (newValue, oldValue) { + if (newValue) { + setActiveView(newValue); + } + }, true); } - }). - bind("blur", function () { - //Reset on focus lost - $(el).data("editmode", false); - }); - }; - }); + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-sub-views.html', + scope: { + subViews: '=', + model: '=' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEditorView +@restrict E +@scope -angular.module("umbraco.directives") - .directive('umbAutoFocus', function($timeout) { - - return function(scope, element, attr){ - var update = function() { - //if it uses its default naming - if(element.val() === "" || attr.focusOnFilled){ - element.focus(); +@description +Use this directive to construct the main editor window. + +

    Markup example

    +
    +    
    + +
    + + + + + + + + // main content here + + + + // footer content here + + + + +
    + +
    +
    +

    Controller example

    +
    +    (function () {
    +
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +
    +        }
    +
    +        angular.module("umbraco").controller("MySection.Controller", Controller);
    +    })();
    +
    + + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
    • +
    • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
    • +
    • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
    • +
    +**/ + (function () { + 'use strict'; + function EditorViewDirective() { + function link(scope, el, attr) { + if (attr.footer) { + scope.footer = attr.footer; } - }; - - $timeout(function() { - update(); - }); - }; -}); - -angular.module("umbraco.directives") - .directive('umbAutoResize', function($timeout) { - return { - require: ["^?umbTabs", "ngModel"], - link: function(scope, element, attr, controllersArr) { - - var domEl = element[0]; - var domElType = domEl.type; - var umbTabsController = controllersArr[0]; - var ngModelController = controllersArr[1]; - - // IE elements - var isIEFlag = false; - var wrapper = angular.element('#umb-ie-resize-input-wrapper'); - var mirror = angular.element(''); - - function isIE() { - - var ua = window.navigator.userAgent; - var msie = ua.indexOf("MSIE "); - - if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./) || navigator.userAgent.match(/Edge\/\d+/)) { - return true; - } else { - return false; - } - - } - - function activate() { - - // check if browser is Internet Explorere - isIEFlag = isIE(); - - // scrollWidth on element does not work in IE on inputs - // we have to do some dirty dom element copying. - if (isIEFlag === true && domElType === "text") { - setupInternetExplorerElements(); - } - } - - function setupInternetExplorerElements() { - - if (!wrapper.length) { - wrapper = angular.element('
    '); - angular.element('body').append(wrapper); - } - - angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', - 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent', - 'boxSizing', 'borderRightWidth', 'borderLeftWidth', 'borderLeftStyle', 'borderRightStyle', - 'paddingLeft', 'paddingRight', 'marginLeft', 'marginRight' - ], function(value) { - mirror.css(value, element.css(value)); - }); - - wrapper.append(mirror); - + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-view.html', + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbEditorView', EditorViewDirective); + }()); + /** +* @description Utillity directives for key and field events +**/ + angular.module('umbraco.directives').directive('onKeyup', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onKeyup); + }; + elm.on('keyup', f); + scope.$on('$destroy', function () { + elm.off('keyup', f); + }); } - - function resizeInternetExplorerInput() { - - mirror.text(element.val() || attr.placeholder); - element.css('width', mirror.outerWidth() + 1); - + }; + }).directive('onKeydown', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onKeydown); + }; + elm.on('keydown', f); + scope.$on('$destroy', function () { + elm.off('keydown', f); + }); } - - function resizeInput() { - - if (domEl.scrollWidth !== domEl.clientWidth) { - if (ngModelController.$modelValue) { - element.width(domEl.scrollWidth); - } - } - - if(!ngModelController.$modelValue && attr.placeholder) { - attr.$set('size', attr.placeholder.length); - element.width('auto'); - } - + }; + }).directive('onBlur', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onBlur); + }; + elm.on('blur', f); + scope.$on('$destroy', function () { + elm.off('blur', f); + }); } - - function resizeTextarea() { - - if(domEl.scrollHeight !== domEl.clientHeight) { - - element.height(domEl.scrollHeight); - - } - + }; + }).directive('onFocus', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onFocus); + }; + elm.on('focus', f); + scope.$on('$destroy', function () { + elm.off('focus', f); + }); } - - var update = function(force) { - - - if (force === true) { - - if (domElType === "textarea") { - element.height(0); - } else if (domElType === "text") { - element.width(0); - } - - } - - - if (isIEFlag === true && domElType === "text") { - - resizeInternetExplorerInput(); - - } else { - - if (domElType === "textarea") { - - resizeTextarea(); - - } else if (domElType === "text") { - - resizeInput(); - - } - - } - - }; - - activate(); - - //listen for tab changes - if (umbTabsController != null) { - umbTabsController.onTabShown(function(args) { - update(); - }); + }; + }).directive('onDragEnter', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragEnter); + }; + elm.on('dragenter', f); + scope.$on('$destroy', function () { + elm.off('dragenter', f); + }); } - - // listen for ng-model changes - var unbindModelWatcher = scope.$watch(function() { - return ngModelController.$modelValue; - }, function(newValue) { - update(true); - }); - - scope.$on('$destroy', function() { - element.unbind('keyup keydown keypress change', update); - element.unbind('blur', update(true)); - unbindModelWatcher(); - - // clean up IE dom element - if (isIEFlag === true && domElType === "text") { - mirror.remove(); - } - - }); - } - }; - }); - -/* -example usage: - -jsonEditing is a string which we edit in a textarea. we try parsing to JSON with each change. when it is valid, propagate model changes via ngModelCtrl - -use isolate scope to prevent model propagation when invalid - will update manually. cannot replace with template, or will override ngModelCtrl, and not hide behind facade - -will override element type to textarea and add own attribute ngModel tied to jsonEditing - */ - -angular.module("umbraco.directives") - .directive('umbRawModel', function () { - return { - restrict: 'A', - require: 'ngModel', - template: '', - replace : true, - scope: { - model: '=umbRawModel', - validateOn:'=' - }, - link: function (scope, element, attrs, ngModelCtrl) { - - function setEditing (value) { - scope.jsonEditing = angular.copy( jsonToString(value)); - } - - function updateModel (value) { - scope.model = stringToJson(value); - } - - function setValid() { - ngModelCtrl.$setValidity('json', true); - } - - function setInvalid () { - ngModelCtrl.$setValidity('json', false); - } - - function stringToJson(text) { - try { - return angular.fromJson(text); - } catch (err) { - setInvalid(); - return text; - } - } - - function jsonToString(object) { - // better than JSON.stringify(), because it formats + filters $$hashKey etc. - // NOTE that this will remove all $-prefixed values - return angular.toJson(object, true); - } - - function isValidJson(model) { - var flag = true; - try { - angular.fromJson(model); - } catch (err) { - flag = false; - } - return flag; - } - - //init - setEditing(scope.model); - - var onInputChange = function(newval,oldval){ - if (newval !== oldval) { - if (isValidJson(newval)) { - setValid(); - updateModel(newval); - } else { - setInvalid(); - } - } - }; - - if(scope.validateOn){ - element.on(scope.validateOn, function(){ - scope.$apply(function(){ - onInputChange(scope.jsonEditing); - }); - }); - }else{ - //check for changes going out - scope.$watch('jsonEditing', onInputChange, true); - } - - //check for changes coming in - scope.$watch('model', function (newval, oldval) { - if (newval !== oldval) { - setEditing(newval); - } - }, true); - - } - }; - }); - -(function() { - 'use strict'; - - function SelectWhen($timeout) { - - function link(scope, el, attr, ctrl) { - - attr.$observe("umbSelectWhen", function(newValue) { - if (newValue === "true") { - $timeout(function() { - el.select(); - }); + }; + }).directive('onDragLeave', function () { + return function (scope, elm, attrs) { + var f = function (event) { + var rect = this.getBoundingClientRect(); + var getXY = function getCursorPosition(event) { + var x, y; + if (typeof event.clientX === 'undefined') { + // try touch screen + x = event.pageX + document.documentElement.scrollLeft; + y = event.pageY + document.documentElement.scrollTop; + } else { + x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + return { + x: x, + y: y + }; + }; + var e = getXY(event.originalEvent); + // Check the mouseEvent coordinates are outside of the rectangle + if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) { + scope.$apply(attrs.onDragLeave); } + }; + elm.on('dragleave', f); + scope.$on('$destroy', function () { + elm.off('dragleave', f); }); - - } - - var directive = { - restrict: 'A', - link: link }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbSelectWhen', SelectWhen); - -})(); - -angular.module("umbraco.directives") - .directive('gridRte', function (tinyMceService, stylesheetResource, angularHelper, assetsService, $q, $timeout) { + }).directive('onDragOver', function () { return { - scope: { - uniqueId: '=', - value: '=', - onClick: '&', - onFocus: '&', - onBlur: '&', - configuration:"=", - onMediaPickerClick: "=", - onEmbedClick: "=", - onMacroPickerClick: "=", - onLinkPickerClick: "=" - }, - template: "", - replace: true, - link: function (scope, element, attrs) { - - var initTiny = function () { - - //we always fetch the default one, and then override parts with our own - tinyMceService.configuration().then(function (tinyMceConfig) { - - - - //config value from general tinymce.config file + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragOver); + }; + elm.on('dragover', f); + scope.$on('$destroy', function () { + elm.off('dragover', f); + }); + } + }; + }).directive('onDragStart', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragStart); + }; + elm.on('dragstart', f); + scope.$on('$destroy', function () { + elm.off('dragstart', f); + }); + } + }; + }).directive('onDragEnd', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragEnd); + }; + elm.on('dragend', f); + scope.$on('$destroy', function () { + elm.off('dragend', f); + }); + } + }; + }).directive('onDrop', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDrop); + }; + elm.on('drop', f); + scope.$on('$destroy', function () { + elm.off('drop', f); + }); + } + }; + }).directive('onOutsideClick', function ($timeout) { + return function (scope, element, attrs) { + var eventBindings = []; + function oneTimeClick(event) { + var el = event.target.nodeName; + //ignore link and button clicks + var els = [ + 'INPUT', + 'A', + 'BUTTON' + ]; + if (els.indexOf(el) >= 0) { + return; + } + // ignore clicks on new overlay + var parents = $(event.target).parents('a,button,.umb-overlay'); + if (parents.length > 0) { + return; + } + // ignore clicks on dialog from old dialog service + var oldDialog = $(event.target).parents('#old-dialog-service'); + if (oldDialog.length === 1) { + return; + } + // ignore clicks in tinyMCE dropdown(floatpanel) + var floatpanel = $(event.target).closest('.mce-floatpanel'); + if (floatpanel.length === 1) { + return; + } + //ignore clicks inside this element + if ($(element).has($(event.target)).length > 0) { + return; + } + scope.$apply(attrs.onOutsideClick); + } + $timeout(function () { + if ('bindClickOn' in attrs) { + eventBindings.push(scope.$watch(function () { + return attrs.bindClickOn; + }, function (newValue) { + if (newValue === 'true') { + $(document).on('click', oneTimeClick); + } else { + $(document).off('click', oneTimeClick); + } + })); + } else { + $(document).on('click', oneTimeClick); + } + scope.$on('$destroy', function () { + $(document).off('click', oneTimeClick); + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } + }); + }); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution. + }; + }).directive('onRightClick', function () { + document.oncontextmenu = function (e) { + if (e.target.hasAttribute('on-right-click')) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }; + return function (scope, el, attrs) { + el.on('contextmenu', function (e) { + e.preventDefault(); + e.stopPropagation(); + scope.$apply(attrs.onRightClick); + return false; + }); + }; + }).directive('onDelayedMouseleave', function ($timeout, $parse) { + return { + restrict: 'A', + link: function (scope, element, attrs, ctrl) { + var active = false; + var fn = $parse(attrs.onDelayedMouseleave); + var leave_f = function (event) { + var callback = function () { + fn(scope, { $event: event }); + }; + active = false; + $timeout(function () { + if (active === false) { + scope.$apply(callback); + } + }, 650); + }; + var enter_f = function (event, args) { + active = true; + }; + element.on('mouseleave', leave_f); + element.on('mouseenter', enter_f); + //unsub events + scope.$on('$destroy', function () { + element.off('mouseleave', leave_f); + element.off('mouseenter', enter_f); + }); + } + }; + }); + /* + + http://vitalets.github.io/checklist-model/ + +*/ + angular.module('umbraco.directives').directive('checklistModel', [ + '$parse', + '$compile', + function ($parse, $compile) { + // contains + function contains(arr, item) { + if (angular.isArray(arr)) { + for (var i = 0; i < arr.length; i++) { + if (angular.equals(arr[i], item)) { + return true; + } + } + } + return false; + } + // add + function add(arr, item) { + arr = angular.isArray(arr) ? arr : []; + for (var i = 0; i < arr.length; i++) { + if (angular.equals(arr[i], item)) { + return arr; + } + } + arr.push(item); + return arr; + } + // remove + function remove(arr, item) { + if (angular.isArray(arr)) { + for (var i = 0; i < arr.length; i++) { + if (angular.equals(arr[i], item)) { + arr.splice(i, 1); + break; + } + } + } + return arr; + } + // http://stackoverflow.com/a/19228302/1458162 + function postLinkFn(scope, elem, attrs) { + // compile with `ng-model` pointing to `checked` + $compile(elem)(scope); + // getter / setter for original model + var getter = $parse(attrs.checklistModel); + var setter = getter.assign; + // value added to list + var value = $parse(attrs.checklistValue)(scope.$parent); + // watch UI checked change + scope.$watch('checked', function (newValue, oldValue) { + if (newValue === oldValue) { + return; + } + var current = getter(scope.$parent); + if (newValue === true) { + setter(scope.$parent, add(current, value)); + } else { + setter(scope.$parent, remove(current, value)); + } + }); + // watch original model change + scope.$parent.$watch(attrs.checklistModel, function (newArr, oldArr) { + scope.checked = contains(newArr, value); + }, true); + } + return { + restrict: 'A', + priority: 1000, + terminal: true, + scope: true, + compile: function (tElement, tAttrs) { + if (tElement[0].tagName !== 'INPUT' || !tElement.attr('type', 'checkbox')) { + throw 'checklist-model should be applied to `input[type="checkbox"]`.'; + } + if (!tAttrs.checklistValue) { + throw 'You should provide `checklist-value`.'; + } + // exclude recursion + tElement.removeAttr('checklist-model'); + // local scope var storing individual checkbox model + tElement.attr('ng-model', 'checked'); + return postLinkFn; + } + }; + } + ]); + angular.module('umbraco.directives').directive('contenteditable', function () { + return { + require: 'ngModel', + link: function (scope, element, attrs, ngModel) { + function read() { + ngModel.$setViewValue(element.html()); + } + ngModel.$render = function () { + element.html(ngModel.$viewValue || ''); + }; + element.bind('focus', function () { + var range = document.createRange(); + range.selectNodeContents(element[0]); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + }); + element.bind('blur keyup change', function () { + scope.$apply(read); + }); + } + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:fixNumber +* @restrict A +* @description Used in conjunction with type='number' input fields to ensure that the bound value is converted to a number when using ng-model +* because normally it thinks it's a string and also validation doesn't work correctly due to an angular bug. +**/ + function fixNumber($parse) { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, elem, attrs, ctrl) { + //parse ngModel onload + var modelVal = scope.$eval(attrs.ngModel); + if (modelVal) { + var asNum = parseFloat(modelVal, 10); + if (!isNaN(asNum)) { + $parse(attrs.ngModel).assign(scope, asNum); + } + } + //always return an int to the model + ctrl.$parsers.push(function (value) { + if (value === 0) { + return 0; + } + return parseFloat(value || '', 10); + }); + //always try to format the model value as an int + ctrl.$formatters.push(function (value) { + if (angular.isString(value)) { + return parseFloat(value, 10); + } + return value; + }); + //This fixes this angular issue: + //https://github.com/angular/angular.js/issues/2144 + // which doesn't actually validate the number input properly since the model only changes when a real number is entered + // but the input box still allows non-numbers to be entered which do not validate (only via html5) + if (typeof elem.prop('validity') === 'undefined') { + return; + } + elem.bind('input', function (e) { + var validity = elem.prop('validity'); + scope.$apply(function () { + ctrl.$setValidity('number', !validity.badInput); + }); + }); + } + }; + } + angular.module('umbraco.directives').directive('fixNumber', fixNumber); + angular.module('umbraco.directives').directive('focusWhen', function ($timeout) { + return { + restrict: 'A', + link: function (scope, elm, attrs, ctrl) { + attrs.$observe('focusWhen', function (newValue) { + if (newValue === 'true') { + $timeout(function () { + elm.focus(); + }); + } + }); + } + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:hexBgColor +* @restrict A +* @description Used to set a hex background color on an element, this will detect valid hex and when it is valid it will set the color, otherwise +* a color will not be set. +**/ + function hexBgColor() { + return { + restrict: 'A', + link: function (scope, element, attr, formCtrl) { + var origColor = null; + if (attr.hexBgOrig) { + //set the orig based on the attribute if there is one + origColor = attr.hexBgOrig; + } + attr.$observe('hexBgColor', function (newVal) { + if (newVal) { + if (!origColor) { + //get the orig color before changing it + origColor = element.css('border-color'); + } + //validate it - test with and without the leading hash. + if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { + element.css('background-color', '#' + newVal); + return; + } + if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { + element.css('background-color', newVal); + return; + } + } + element.css('background-color', origColor); + }); + } + }; + } + angular.module('umbraco.directives').directive('hexBgColor', hexBgColor); + /** +* @ngdoc directive +* @name umbraco.directives.directive:hotkey +**/ + angular.module('umbraco.directives').directive('hotkey', function ($window, keyboardService, $log) { + return function (scope, el, attrs) { + var options = {}; + var keyCombo = attrs.hotkey; + if (!keyCombo) { + //support data binding + keyCombo = scope.$eval(attrs['hotkey']); + } + function activate() { + if (keyCombo) { + // disable shortcuts in input fields if keycombo is 1 character + if (keyCombo.length === 1) { + options = { inputDisabled: true }; + } + keyboardService.bind(keyCombo, function () { + var element = $(el); + var activeElementType = document.activeElement.tagName; + var clickableElements = [ + 'A', + 'BUTTON' + ]; + if (element.is('a,div,button,input[type=\'button\'],input[type=\'submit\'],input[type=\'checkbox\']') && !element.is(':disabled')) { + if (element.is(':visible') || attrs.hotkeyWhenHidden) { + if (attrs.hotkeyWhen && attrs.hotkeyWhen === 'false') { + return; + } + // when keycombo is enter and a link or button has focus - click the link or button instead of using the hotkey + if (keyCombo === 'enter' && clickableElements.indexOf(activeElementType) === 0) { + document.activeElement.click(); + } else { + element.click(); + } + } + } else { + element.focus(); + } + }, options); + el.on('$destroy', function () { + keyboardService.unbind(keyCombo); + }); + } + } + activate(); + }; + }); + /** +@ngdoc directive +@name umbraco.directives.directive:preventDefault + +@description +Use this directive to prevent default action of an element. Effectively implementing jQuery's preventdefault + +

    Markup example

    + +
    +    Don't go to Umbraco.com
    +
    + +**/ + angular.module('umbraco.directives').directive('preventDefault', function () { + return function (scope, element, attrs) { + var enabled = true; + //check if there's a value for the attribute, if there is and it's false then we conditionally don't + //prevent default. + if (attrs.preventDefault) { + attrs.$observe('preventDefault', function (newVal) { + enabled = newVal === 'false' || newVal === 0 || newVal === false ? false : true; + }); + } + $(element).click(function (event) { + if (event.metaKey || event.ctrlKey) { + return; + } else { + if (enabled === true) { + event.preventDefault(); + } + } + }); + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:preventEnterSubmit +* @description prevents a form from submitting when the enter key is pressed on an input field +**/ + angular.module('umbraco.directives').directive('preventEnterSubmit', function () { + return function (scope, element, attrs) { + var enabled = true; + //check if there's a value for the attribute, if there is and it's false then we conditionally don't + //prevent default. + if (attrs.preventEnterSubmit) { + attrs.$observe('preventEnterSubmit', function (newVal) { + enabled = newVal === 'false' || newVal === 0 || newVal === false ? false : true; + }); + } + $(element).keypress(function (event) { + if (event.which === 13) { + event.preventDefault(); + } + }); + }; + }); + /** + * @ngdoc directive + * @name umbraco.directives.directive:resizeToContent + * @element div + * @function + * + * @description + * Resize iframe's automatically to fit to the content they contain + * + * @example + + + + + + */ + angular.module('umbraco.directives').directive('resizeToContent', function ($window, $timeout) { + return function (scope, el, attrs) { + var iframe = el[0]; + var iframeWin = iframe.contentWindow || iframe.contentDocument.parentWindow; + if (iframeWin.document.body) { + $timeout(function () { + var height = iframeWin.document.documentElement.scrollHeight || iframeWin.document.body.scrollHeight; + el.height(height); + }, 3000); + } + }; + }); + angular.module('umbraco.directives').directive('selectOnFocus', function () { + return function (scope, el, attrs) { + $(el).bind('click', function () { + var editmode = $(el).data('editmode'); + //If editmode is true a click is handled like a normal click + if (!editmode) { + //Initial click, select entire text + this.select(); + //Set the edit mode so subsequent clicks work normally + $(el).data('editmode', true); + } + }).bind('blur', function () { + //Reset on focus lost + $(el).data('editmode', false); + }); + }; + }); + angular.module('umbraco.directives').directive('umbAutoFocus', function ($timeout) { + return function (scope, element, attr) { + var update = function () { + //if it uses its default naming + if (element.val() === '' || attr.focusOnFilled) { + element.focus(); + } + }; + $timeout(function () { + update(); + }); + }; + }); + angular.module('umbraco.directives').directive('umbAutoResize', function ($timeout) { + return { + require: [ + '^?umbTabs', + 'ngModel' + ], + link: function (scope, element, attr, controllersArr) { + var domEl = element[0]; + var domElType = domEl.type; + var umbTabsController = controllersArr[0]; + var ngModelController = controllersArr[1]; + // IE elements + var isIEFlag = false; + var wrapper = angular.element('#umb-ie-resize-input-wrapper'); + var mirror = angular.element(''); + function isIE() { + var ua = window.navigator.userAgent; + var msie = ua.indexOf('MSIE '); + if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./) || navigator.userAgent.match(/Edge\/\d+/)) { + return true; + } else { + return false; + } + } + function activate() { + // check if browser is Internet Explorere + isIEFlag = isIE(); + // scrollWidth on element does not work in IE on inputs + // we have to do some dirty dom element copying. + if (isIEFlag === true && domElType === 'text') { + setupInternetExplorerElements(); + } + } + function setupInternetExplorerElements() { + if (!wrapper.length) { + wrapper = angular.element('
    '); + angular.element('body').append(wrapper); + } + angular.forEach([ + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'letterSpacing', + 'textTransform', + 'wordSpacing', + 'textIndent', + 'boxSizing', + 'borderRightWidth', + 'borderLeftWidth', + 'borderLeftStyle', + 'borderRightStyle', + 'paddingLeft', + 'paddingRight', + 'marginLeft', + 'marginRight' + ], function (value) { + mirror.css(value, element.css(value)); + }); + wrapper.append(mirror); + } + function resizeInternetExplorerInput() { + mirror.text(element.val() || attr.placeholder); + element.css('width', mirror.outerWidth() + 1); + } + function resizeInput() { + if (domEl.scrollWidth !== domEl.clientWidth) { + if (ngModelController.$modelValue) { + element.width(domEl.scrollWidth); + } + } + if (!ngModelController.$modelValue && attr.placeholder) { + attr.$set('size', attr.placeholder.length); + element.width('auto'); + } + } + function resizeTextarea() { + if (domEl.scrollHeight !== domEl.clientHeight) { + element.height(domEl.scrollHeight); + } + } + var update = function (force) { + if (force === true) { + if (domElType === 'textarea') { + element.height(0); + } else if (domElType === 'text') { + element.width(0); + } + } + if (isIEFlag === true && domElType === 'text') { + resizeInternetExplorerInput(); + } else { + if (domElType === 'textarea') { + resizeTextarea(); + } else if (domElType === 'text') { + resizeInput(); + } + } + }; + activate(); + //listen for tab changes + if (umbTabsController != null) { + umbTabsController.onTabShown(function (args) { + update(); + }); + } + // listen for ng-model changes + var unbindModelWatcher = scope.$watch(function () { + return ngModelController.$modelValue; + }, function (newValue) { + update(true); + }); + scope.$on('$destroy', function () { + element.unbind('keyup keydown keypress change', update); + element.unbind('blur', update(true)); + unbindModelWatcher(); + // clean up IE dom element + if (isIEFlag === true && domElType === 'text') { + mirror.remove(); + } + }); + } + }; + }); + /* +example usage: + +jsonEditing is a string which we edit in a textarea. we try parsing to JSON with each change. when it is valid, propagate model changes via ngModelCtrl + +use isolate scope to prevent model propagation when invalid - will update manually. cannot replace with template, or will override ngModelCtrl, and not hide behind facade + +will override element type to textarea and add own attribute ngModel tied to jsonEditing + */ + angular.module('umbraco.directives').directive('umbRawModel', function () { + return { + restrict: 'A', + require: 'ngModel', + template: '', + replace: true, + scope: { + model: '=umbRawModel', + validateOn: '=' + }, + link: function (scope, element, attrs, ngModelCtrl) { + function setEditing(value) { + scope.jsonEditing = angular.copy(jsonToString(value)); + } + function updateModel(value) { + scope.model = stringToJson(value); + } + function setValid() { + ngModelCtrl.$setValidity('json', true); + } + function setInvalid() { + ngModelCtrl.$setValidity('json', false); + } + function stringToJson(text) { + try { + return angular.fromJson(text); + } catch (err) { + setInvalid(); + return text; + } + } + function jsonToString(object) { + // better than JSON.stringify(), because it formats + filters $$hashKey etc. + // NOTE that this will remove all $-prefixed values + return angular.toJson(object, true); + } + function isValidJson(model) { + var flag = true; + try { + angular.fromJson(model); + } catch (err) { + flag = false; + } + return flag; + } + //init + setEditing(scope.model); + var onInputChange = function (newval, oldval) { + if (newval !== oldval) { + if (isValidJson(newval)) { + setValid(); + updateModel(newval); + } else { + setInvalid(); + } + } + }; + if (scope.validateOn) { + element.on(scope.validateOn, function () { + scope.$apply(function () { + onInputChange(scope.jsonEditing); + }); + }); + } else { + //check for changes going out + scope.$watch('jsonEditing', onInputChange, true); + } + //check for changes coming in + scope.$watch('model', function (newval, oldval) { + if (newval !== oldval) { + setEditing(newval); + } + }, true); + } + }; + }); + (function () { + 'use strict'; + function SelectWhen($timeout) { + function link(scope, el, attr, ctrl) { + attr.$observe('umbSelectWhen', function (newValue) { + if (newValue === 'true') { + $timeout(function () { + el.select(); + }); + } + }); + } + var directive = { + restrict: 'A', + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbSelectWhen', SelectWhen); + }()); + angular.module('umbraco.directives').directive('gridRte', function (tinyMceService, stylesheetResource, angularHelper, assetsService, $q, $timeout) { + return { + scope: { + uniqueId: '=', + value: '=', + onClick: '&', + onFocus: '&', + onBlur: '&', + configuration: '=', + onMediaPickerClick: '=', + onEmbedClick: '=', + onMacroPickerClick: '=', + onLinkPickerClick: '=' + }, + template: '', + replace: true, + link: function (scope, element, attrs) { + var initTiny = function () { + //we always fetch the default one, and then override parts with our own + tinyMceService.configuration().then(function (tinyMceConfig) { + //config value from general tinymce.config file var validElements = tinyMceConfig.validElements; - var fallbackStyles = [{title: "Page header", block: "h2"}, {title: "Section header", block: "h3"}, {title: "Paragraph header", block: "h4"}, {title: "Normal", block: "p"}, {title: "Quote", block: "blockquote"}, {title: "Code", block: "code"}]; - + var fallbackStyles = [ + { + title: 'Page header', + block: 'h2' + }, + { + title: 'Section header', + block: 'h3' + }, + { + title: 'Paragraph header', + block: 'h4' + }, + { + title: 'Normal', + block: 'p' + }, + { + title: 'Quote', + block: 'blockquote' + }, + { + title: 'Code', + block: 'code' + } + ]; //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce - var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]"; - + var extendedValidElements = '@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]'; var invalidElements = tinyMceConfig.inValidElements; var plugins = _.map(tinyMceConfig.plugins, function (plugin) { if (plugin.useOnFrontend) { return plugin.name; } - }).join(" ") + " autoresize"; - + }).join(' ') + ' autoresize'; //config value on the data type - var toolbar = ["code", "styleselect", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "link", "umbmediapicker", "umbembeddialog"].join(" | "); + var toolbar = [ + 'code', + 'styleselect', + 'bold', + 'italic', + 'alignleft', + 'aligncenter', + 'alignright', + 'bullist', + 'numlist', + 'link', + 'umbmediapicker', + 'umbembeddialog' + ].join(' | '); var stylesheets = []; - var styleFormats = []; var await = []; - //queue file loading - if (typeof (tinymce) === "undefined") { - await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", scope)); + if (typeof tinymce === 'undefined') { + await.push(assetsService.loadJs('lib/tinymce/tinymce.min.js', scope)); + } + if (scope.configuration && scope.configuration.toolbar) { + toolbar = scope.configuration.toolbar.join(' | '); + } + if (scope.configuration && scope.configuration.stylesheets) { + angular.forEach(scope.configuration.stylesheets, function (stylesheet, key) { + stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + '/' + stylesheet + '.css'); + await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) { + angular.forEach(rules, function (rule) { + var r = {}; + var split = ''; + r.title = rule.name; + if (rule.selector[0] === '.') { + r.inline = 'span'; + r.classes = rule.selector.substring(1); + } else if (rule.selector[0] === '#') { + //Even though this will render in the style drop down, it will not actually be applied + // to the elements, don't think TinyMCE even supports this and it doesn't really make much sense + // since only one element can have one id. + r.inline = 'span'; + r.attributes = { id: rule.selector.substring(1) }; + } else if (rule.selector[0] !== '.' && rule.selector.indexOf('.') > -1) { + split = rule.selector.split('.'); + r.block = split[0]; + r.classes = rule.selector.substring(rule.selector.indexOf('.') + 1).replace('.', ' '); + } else if (rule.selector[0] !== '#' && rule.selector.indexOf('#') > -1) { + split = rule.selector.split('#'); + r.block = split[0]; + r.classes = rule.selector.substring(rule.selector.indexOf('#') + 1); + } else { + r.block = rule.selector; + } + styleFormats.push(r); + }); + })); + }); + } else { + stylesheets.push('views/propertyeditors/grid/config/grid.default.rtestyles.css'); + styleFormats = fallbackStyles; + } + //stores a reference to the editor + var tinyMceEditor = null; + $q.all(await).then(function () { + var uniqueId = scope.uniqueId; + //create a baseline Config to exten upon + var baseLineConfigObj = { + mode: 'exact', + skin: 'umbraco', + plugins: plugins, + valid_elements: validElements, + invalid_elements: invalidElements, + extended_valid_elements: extendedValidElements, + menubar: false, + statusbar: false, + relative_urls: false, + toolbar: toolbar, + content_css: stylesheets, + style_formats: styleFormats, + autoresize_bottom_margin: 0 + }; + if (tinyMceConfig.customConfig) { + //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to + // convert it to json instead of having it as a string since this is what tinymce requires + for (var i in tinyMceConfig.customConfig) { + var val = tinyMceConfig.customConfig[i]; + if (val) { + val = val.toString().trim(); + if (val.detectIsJson()) { + try { + tinyMceConfig.customConfig[i] = JSON.parse(val); + //now we need to check if this custom config key is defined in our baseline, if it is we don't want to + //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise + //if it's an object it will overwrite the baseline + if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { + //concat it and below this concat'd array will overwrite the baseline in angular.extend + tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]); + } + } catch (e) { + } + } + } + } + angular.extend(baseLineConfigObj, tinyMceConfig.customConfig); + } + //set all the things that user configs should not be able to override + baseLineConfigObj.elements = uniqueId; + baseLineConfigObj.setup = function (editor) { + //set the reference + tinyMceEditor = editor; + //enable browser based spell checking + editor.on('init', function (e) { + editor.getBody().setAttribute('spellcheck', true); + //force overflow to hidden to prevent no needed scroll + editor.getBody().style.overflow = 'hidden'; + $timeout(function () { + if (scope.value === null) { + editor.focus(); + } + }, 400); + }); + // pin toolbar to top of screen if we have focus and it scrolls off the screen + var pinToolbar = function () { + var _toolbar = $(editor.editorContainer).find('.mce-toolbar'); + var toolbarHeight = _toolbar.height(); + var _tinyMce = $(editor.editorContainer); + var tinyMceRect = _tinyMce[0].getBoundingClientRect(); + var tinyMceTop = tinyMceRect.top; + var tinyMceBottom = tinyMceRect.bottom; + var tinyMceWidth = tinyMceRect.width; + var _tinyMceEditArea = _tinyMce.find('.mce-edit-area'); + // set padding in top of mce so the content does not "jump" up + _tinyMceEditArea.css('padding-top', toolbarHeight); + if (tinyMceTop < 160 && 160 + toolbarHeight < tinyMceBottom) { + _toolbar.css('visibility', 'visible').css('position', 'fixed').css('top', '160px').css('margin-top', '0').css('width', tinyMceWidth); + } else { + _toolbar.css('visibility', 'visible').css('position', 'absolute').css('top', 'auto').css('margin-top', '0').css('width', tinyMceWidth); + } + }; + // unpin toolbar to top of screen + var unpinToolbar = function () { + var _toolbar = $(editor.editorContainer).find('.mce-toolbar'); + var _tinyMce = $(editor.editorContainer); + var _tinyMceEditArea = _tinyMce.find('.mce-edit-area'); + // reset padding in top of mce so the content does not "jump" up + _tinyMceEditArea.css('padding-top', '0'); + _toolbar.css('position', 'static'); + }; + //when we leave the editor (maybe) + editor.on('blur', function (e) { + editor.save(); + angularHelper.safeApply(scope, function () { + scope.value = editor.getContent(); + var _toolbar = $(editor.editorContainer).find('.mce-toolbar'); + if (scope.onBlur) { + scope.onBlur(); + } + unpinToolbar(); + $('.umb-panel-body').off('scroll', pinToolbar); + }); + }); + // Focus on editor + editor.on('focus', function (e) { + angularHelper.safeApply(scope, function () { + if (scope.onFocus) { + scope.onFocus(); + } + pinToolbar(); + $('.umb-panel-body').on('scroll', pinToolbar); + }); + }); + // Click on editor + editor.on('click', function (e) { + angularHelper.safeApply(scope, function () { + if (scope.onClick) { + scope.onClick(); + } + pinToolbar(); + $('.umb-panel-body').on('scroll', pinToolbar); + }); + }); + //when buttons modify content + editor.on('ExecCommand', function (e) { + editor.save(); + angularHelper.safeApply(scope, function () { + scope.value = editor.getContent(); + }); + }); + // Update model on keypress + editor.on('KeyUp', function (e) { + editor.save(); + angularHelper.safeApply(scope, function () { + scope.value = editor.getContent(); + }); + }); + // Update model on change, i.e. copy/pasted text, plugins altering content + editor.on('SetContent', function (e) { + if (!e.initial) { + editor.save(); + angularHelper.safeApply(scope, function () { + scope.value = editor.getContent(); + }); + } + }); + editor.on('ObjectResized', function (e) { + var qs = '?width=' + e.width + '&height=' + e.height; + var srcAttr = $(e.target).attr('src'); + var path = srcAttr.split('?')[0]; + $(e.target).attr('data-mce-src', path + qs); + }); + //Create the insert link plugin + tinyMceService.createLinkPicker(editor, scope, function (currentTarget, anchorElement) { + if (scope.onLinkPickerClick) { + scope.onLinkPickerClick(editor, currentTarget, anchorElement); + } + }); + //Create the insert media plugin + tinyMceService.createMediaPicker(editor, scope, function (currentTarget, userData) { + if (scope.onMediaPickerClick) { + scope.onMediaPickerClick(editor, currentTarget, userData); + } + }); + //Create the embedded plugin + tinyMceService.createInsertEmbeddedMedia(editor, scope, function () { + if (scope.onEmbedClick) { + scope.onEmbedClick(editor); + } + }); + //Create the insert macro plugin + tinyMceService.createInsertMacro(editor, scope, function (dialogData) { + if (scope.onMacroPickerClick) { + scope.onMacroPickerClick(editor, dialogData); + } + }); + }; + /** Loads in the editor */ + function loadTinyMce() { + //we need to add a timeout here, to force a redraw so TinyMCE can find + //the elements needed + $timeout(function () { + tinymce.DOM.events.domLoaded = true; + tinymce.init(baseLineConfigObj); + }, 150, false); + } + loadTinyMce(); + //here we declare a special method which will be called whenever the value has changed from the server + //this is instead of doing a watch on the model.value = faster + //scope.model.onValueChanged = function (newVal, oldVal) { + // //update the display val again if it has changed from the server; + // tinyMceEditor.setContent(newVal, { format: 'raw' }); + // //we need to manually fire this event since it is only ever fired based on loading from the DOM, this + // // is required for our plugins listening to this event to execute + // tinyMceEditor.fire('LoadContent', null); + //}; + //listen for formSubmitting event (the result is callback used to remove the event subscription) + var unsubscribe = scope.$on('formSubmitting', function () { + //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer + // we do parse it out on the server side but would be nice to do that on the client side before as well. + scope.value = tinyMceEditor ? tinyMceEditor.getContent() : null; + }); + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom + // element might still be there even after the modal has been hidden. + scope.$on('$destroy', function () { + unsubscribe(); + }); + }); + }); + }; + initTiny(); + } + }; + }); + /** +@ngdoc directive +@name umbraco.directives.directive:umbBox +@restrict E + +@description +Use this directive to render an already styled empty div tag. + +

    Markup example

    +
    +    
    +        
    +        
    +            // Content here
    +        
    +    
    +
    + +

    Use in combination with:

    +
      +
    • {@link umbraco.directives.directive:umbBoxHeader umbBoxHeader}
    • +
    • {@link umbraco.directives.directive:umbBoxContent umbBoxContent}
    • +
    +**/ + (function () { + 'use strict'; + function BoxDirective() { + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/html/umb-box/umb-box.html' + }; + return directive; + } + angular.module('umbraco.directives').directive('umbBox', BoxDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbBoxContent +@restrict E + +@description +Use this directive to render an empty container. Recommended to use it inside an {@link umbraco.directives.directive:umbBox umbBox} directive. See documentation for {@link umbraco.directives.directive:umbBox umbBox}. + +

    Markup example

    +
    +    
    +        
    +        
    +            // Content here
    +        
    +    
    +
    + +

    Use in combination with:

    +
      +
    • {@link umbraco.directives.directive:umbBox umbBox}
    • +
    • {@link umbraco.directives.directive:umbBoxHeader umbBoxHeader}
    • +
    +**/ + (function () { + 'use strict'; + function BoxContentDirective() { + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/html/umb-box/umb-box-content.html' + }; + return directive; + } + angular.module('umbraco.directives').directive('umbBoxContent', BoxContentDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbBoxHeader +@restrict E +@scope + +@description +Use this directive to construct a title. Recommended to use it inside an {@link umbraco.directives.directive:umbBox umbBox} directive. See documentation for {@link umbraco.directives.directive:umbBox umbBox}. + +

    Markup example

    +
    +    
    +        
    +        
    +            // Content here
    +        
    +    
    +
    + +

    Markup example with using titleKey

    +
    +    
    +        // the title-key property needs an areaAlias_keyAlias from the language files
    +        
    +        
    +            // Content here
    +        
    +    
    +
    +{@link https://our.umbraco.org/documentation/extending/language-files/ Here you can see more about the language files} + +

    Use in combination with:

    +
      +
    • {@link umbraco.directives.directive:umbBox umbBox}
    • +
    • {@link umbraco.directives.directive:umbBoxContent umbBoxContent}
    • +
    + +@param {string=} title (attrbute): Custom title text. +@param {string=} titleKey (attrbute): The translation key from the language xml files. +@param {string=} description (attrbute): Custom description text. +@param {string=} descriptionKey (attrbute): The translation key from the language xml files. +**/ + (function () { + 'use strict'; + function BoxHeaderDirective() { + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/html/umb-box/umb-box-header.html', + scope: { + titleKey: '@?', + title: '@?', + descriptionKey: '@?', + description: '@?' + } + }; + return directive; + } + angular.module('umbraco.directives').directive('umbBoxHeader', BoxHeaderDirective); + }()); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbControlGroup +* @restrict E +**/ + angular.module('umbraco.directives.html').directive('umbControlGroup', function (localizationService) { + return { + scope: { + label: '@label', + description: '@', + hideLabel: '@', + alias: '@', + labelFor: '@', + required: '@?' + }, + require: '?^form', + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/html/umb-control-group.html', + link: function (scope, element, attr, formCtrl) { + scope.formValid = function () { + if (formCtrl && scope.labelFor) { + //if a label-for has been set, use that for the validation + return formCtrl[scope.labelFor].$valid; + } + //there is no form. + return true; + }; + if (scope.label && scope.label[0] === '@') { + scope.labelstring = localizationService.localize(scope.label.substring(1)); + } else { + scope.labelstring = scope.label; + } + if (scope.description && scope.description[0] === '@') { + scope.descriptionstring = localizationService.localize(scope.description.substring(1)); + } else { + scope.descriptionstring = scope.description; + } + } + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbPane +* @restrict E +**/ + angular.module('umbraco.directives.html').directive('umbPane', function () { + return { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/html/umb-pane.html' + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbPanel +* @restrict E +**/ + angular.module('umbraco.directives.html').directive('umbPanel', function ($timeout, $log) { + return { + restrict: 'E', + replace: true, + transclude: 'true', + templateUrl: 'views/components/html/umb-panel.html' + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbImageCrop +* @restrict E +* @function +**/ + angular.module('umbraco.directives').directive('umbImageCrop', function ($timeout, localizationService, cropperHelper, $log) { + return { + restrict: 'E', + replace: true, + templateUrl: 'views/components/imaging/umb-image-crop.html', + scope: { + src: '=', + width: '@', + height: '@', + crop: '=', + center: '=', + maxSize: '@' + }, + link: function (scope, element, attrs) { + scope.width = 400; + scope.height = 320; + scope.dimensions = { + image: {}, + cropper: {}, + viewport: {}, + margin: 20, + scale: { + min: 0.3, + max: 3, + current: 1 + } + }; + //live rendering of viewport and image styles + scope.style = function () { + return { + 'height': parseInt(scope.dimensions.viewport.height, 10) + 'px', + 'width': parseInt(scope.dimensions.viewport.width, 10) + 'px' + }; + }; + //elements + var $viewport = element.find('.viewport'); + var $image = element.find('img'); + var $overlay = element.find('.overlay'); + var $container = element.find('.crop-container'); + //default constraints for drag n drop + var constraints = { + left: { + max: scope.dimensions.margin, + min: scope.dimensions.margin + }, + top: { + max: scope.dimensions.margin, + min: scope.dimensions.margin + } + }; + scope.constraints = constraints; + //set constaints for cropping drag and drop + var setConstraints = function () { + constraints.left.min = scope.dimensions.margin + scope.dimensions.cropper.width - scope.dimensions.image.width; + constraints.top.min = scope.dimensions.margin + scope.dimensions.cropper.height - scope.dimensions.image.height; + }; + var setDimensions = function (originalImage) { + originalImage.width('auto'); + originalImage.height('auto'); + var image = {}; + image.originalWidth = originalImage.width(); + image.originalHeight = originalImage.height(); + image.width = image.originalWidth; + image.height = image.originalHeight; + image.left = originalImage[0].offsetLeft; + image.top = originalImage[0].offsetTop; + scope.dimensions.image = image; + //unscaled editor size + //var viewPortW = $viewport.width(); + //var viewPortH = $viewport.height(); + var _viewPortW = parseInt(scope.width, 10); + var _viewPortH = parseInt(scope.height, 10); + //if we set a constraint we will scale it down if needed + if (scope.maxSize) { + var ratioCalculation = cropperHelper.scaleToMaxSize(_viewPortW, _viewPortH, scope.maxSize); + //so if we have a max size, override the thumb sizes + _viewPortW = ratioCalculation.width; + _viewPortH = ratioCalculation.height; + } + scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin; + scope.dimensions.viewport.height = _viewPortH + 2 * scope.dimensions.margin; + scope.dimensions.cropper.width = _viewPortW; + // scope.dimensions.viewport.width - 2 * scope.dimensions.margin; + scope.dimensions.cropper.height = _viewPortH; // scope.dimensions.viewport.height - 2 * scope.dimensions.margin; + }; + //when loading an image without any crop info, we center and fit it + var resizeImageToEditor = function () { + //returns size fitting the cropper + var size = cropperHelper.calculateAspectRatioFit(scope.dimensions.image.width, scope.dimensions.image.height, scope.dimensions.cropper.width, scope.dimensions.cropper.height, true); + //sets the image size and updates the scope + scope.dimensions.image.width = size.width; + scope.dimensions.image.height = size.height; + //calculate the best suited ratios + scope.dimensions.scale.min = size.ratio; + scope.dimensions.scale.max = 2; + scope.dimensions.scale.current = size.ratio; + //center the image + var position = cropperHelper.centerInsideViewPort(scope.dimensions.image, scope.dimensions.cropper); + scope.dimensions.top = position.top; + scope.dimensions.left = position.left; + setConstraints(); + }; + //resize to a given ratio + var resizeImageToScale = function (ratio) { + //do stuff + var size = cropperHelper.calculateSizeToRatio(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, ratio); + scope.dimensions.image.width = size.width; + scope.dimensions.image.height = size.height; + setConstraints(); + validatePosition(scope.dimensions.image.left, scope.dimensions.image.top); + }; + //resize the image to a predefined crop coordinate + var resizeImageToCrop = function () { + scope.dimensions.image = cropperHelper.convertToStyle(scope.crop, { + width: scope.dimensions.image.originalWidth, + height: scope.dimensions.image.originalHeight + }, scope.dimensions.cropper, scope.dimensions.margin); + var ratioCalculation = cropperHelper.calculateAspectRatioFit(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, scope.dimensions.cropper.width, scope.dimensions.cropper.height, true); + scope.dimensions.scale.current = scope.dimensions.image.ratio; + //min max based on original width/height + scope.dimensions.scale.min = ratioCalculation.ratio; + scope.dimensions.scale.max = 2; + }; + var validatePosition = function (left, top) { + if (left > constraints.left.max) { + left = constraints.left.max; + } + if (left <= constraints.left.min) { + left = constraints.left.min; + } + if (top > constraints.top.max) { + top = constraints.top.max; + } + if (top <= constraints.top.min) { + top = constraints.top.min; + } + if (scope.dimensions.image.left !== left) { + scope.dimensions.image.left = left; + } + if (scope.dimensions.image.top !== top) { + scope.dimensions.image.top = top; + } + }; + //sets scope.crop to the recalculated % based crop + var calculateCropBox = function () { + scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); + }; + //Drag and drop positioning, using jquery ui draggable + var onStartDragPosition, top, left; + $overlay.draggable({ + drag: function (event, ui) { + scope.$apply(function () { + validatePosition(ui.position.left, ui.position.top); + }); + }, + stop: function (event, ui) { + scope.$apply(function () { + //make sure that every validates one more time... + validatePosition(ui.position.left, ui.position.top); + calculateCropBox(); + scope.dimensions.image.rnd = Math.random(); + }); + } + }); + var init = function (image) { + scope.loaded = false; + //set dimensions on image, viewport, cropper etc + setDimensions(image); + //if we have a crop already position the image + if (scope.crop) { + resizeImageToCrop(); + } else { + resizeImageToEditor(); + } + //sets constaints for the cropper + setConstraints(); + scope.loaded = true; + }; + /// WATCHERS //// + scope.$watchCollection('[width, height]', function (newValues, oldValues) { + //we have to reinit the whole thing if + //one of the external params changes + if (newValues !== oldValues) { + setDimensions($image); + setConstraints(); + } + }); + var throttledResizing = _.throttle(function () { + resizeImageToScale(scope.dimensions.scale.current); + calculateCropBox(); + }, 100); + //happens when we change the scale + scope.$watch('dimensions.scale.current', function () { + if (scope.loaded) { + throttledResizing(); + } + }); + //ie hack + if (window.navigator.userAgent.indexOf('MSIE ')) { + var ranger = element.find('input'); + ranger.bind('change', function () { + scope.$apply(function () { + scope.dimensions.scale.current = ranger.val(); + }); + }); + } + //// INIT ///// + $image.load(function () { + $timeout(function () { + init($image); + }); + }); + } + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbImageGravity +* @restrict E +* @function +* @description +**/ + angular.module('umbraco.directives').directive('umbImageGravity', function ($timeout, localizationService, $log) { + return { + restrict: 'E', + replace: true, + templateUrl: 'views/components/imaging/umb-image-gravity.html', + scope: { + src: '=', + center: '=', + onImageLoaded: '=' + }, + link: function (scope, element, attrs) { + //Internal values for keeping track of the dot and the size of the editor + scope.dimensions = { + width: 0, + height: 0, + left: 0, + top: 0 + }; + scope.loaded = false; + //elements + var $viewport = element.find('.viewport'); + var $image = element.find('img'); + var $overlay = element.find('.overlay'); + scope.style = function () { + if (scope.dimensions.width <= 0) { + setDimensions(); + } + return { + 'top': scope.dimensions.top + 'px', + 'left': scope.dimensions.left + 'px' + }; + }; + scope.setFocalPoint = function (event) { + scope.$emit('imageFocalPointStart'); + var offsetX = event.offsetX - 10; + var offsetY = event.offsetY - 10; + calculateGravity(offsetX, offsetY); + lazyEndEvent(); + }; + var setDimensions = function () { + scope.dimensions.width = $image.width(); + scope.dimensions.height = $image.height(); + if (scope.center) { + scope.dimensions.left = scope.center.left * scope.dimensions.width - 10; + scope.dimensions.top = scope.center.top * scope.dimensions.height - 10; + } else { + scope.center = { + left: 0.5, + top: 0.5 + }; + } + }; + var calculateGravity = function (offsetX, offsetY) { + scope.dimensions.left = offsetX; + scope.dimensions.top = offsetY; + scope.center.left = (scope.dimensions.left + 10) / scope.dimensions.width; + scope.center.top = (scope.dimensions.top + 10) / scope.dimensions.height; + }; + var lazyEndEvent = _.debounce(function () { + scope.$apply(function () { + scope.$emit('imageFocalPointStop'); + }); + }, 2000); + //Drag and drop positioning, using jquery ui draggable + //TODO ensure that the point doesnt go outside the box + $overlay.draggable({ + containment: 'parent', + start: function () { + scope.$apply(function () { + scope.$emit('imageFocalPointStart'); + }); + }, + stop: function () { + scope.$apply(function () { + var offsetX = $overlay[0].offsetLeft; + var offsetY = $overlay[0].offsetTop; + calculateGravity(offsetX, offsetY); + }); + lazyEndEvent(); + } + }); + //// INIT ///// + $image.load(function () { + $timeout(function () { + setDimensions(); + scope.loaded = true; + if (angular.isFunction(scope.onImageLoaded)) { + scope.onImageLoaded(); } - - - if(scope.configuration && scope.configuration.toolbar){ - toolbar = scope.configuration.toolbar.join(' | '); + }); + }); + $(window).on('resize.umbImageGravity', function () { + scope.$apply(function () { + $timeout(function () { + setDimensions(); + }); + // Make sure we can find the offset values for the overlay(dot) before calculating + // fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte) + if ($overlay.is(':visible')) { + var offsetX = $overlay[0].offsetLeft; + var offsetY = $overlay[0].offsetTop; + calculateGravity(offsetX, offsetY); } - - - if(scope.configuration && scope.configuration.stylesheets){ - angular.forEach(scope.configuration.stylesheets, function(stylesheet, key){ - - stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + stylesheet + ".css"); - await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) { - angular.forEach(rules, function (rule) { - var r = {}; - var split = ""; - r.title = rule.name; - if (rule.selector[0] === ".") { - r.inline = "span"; - r.classes = rule.selector.substring(1); - }else if (rule.selector[0] === "#") { - //Even though this will render in the style drop down, it will not actually be applied - // to the elements, don't think TinyMCE even supports this and it doesn't really make much sense - // since only one element can have one id. - r.inline = "span"; - r.attributes = { id: rule.selector.substring(1) }; - }else if (rule.selector[0] !== "." && rule.selector.indexOf(".") > -1) { - split = rule.selector.split("."); - r.block = split[0]; - r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(".", " "); - }else if (rule.selector[0] !== "#" && rule.selector.indexOf("#") > -1) { - split = rule.selector.split("#"); - r.block = split[0]; - r.classes = rule.selector.substring(rule.selector.indexOf("#") + 1); - }else { - r.block = rule.selector; - } - styleFormats.push(r); - }); - })); + }); + }); + scope.$on('$destroy', function () { + $(window).off('.umbImageGravity'); + }); + } + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbImageThumbnail +* @restrict E +* @function +* @description +**/ + angular.module('umbraco.directives').directive('umbImageThumbnail', function ($timeout, localizationService, cropperHelper, $log) { + return { + restrict: 'E', + replace: true, + templateUrl: 'views/components/imaging/umb-image-thumbnail.html', + scope: { + src: '=', + width: '@', + height: '@', + center: '=', + crop: '=', + maxSize: '@' + }, + link: function (scope, element, attrs) { + //// INIT ///// + var $image = element.find('img'); + scope.loaded = false; + $image.load(function () { + $timeout(function () { + $image.width('auto'); + $image.height('auto'); + scope.image = {}; + scope.image.width = $image[0].width; + scope.image.height = $image[0].height; + //we force a lower thumbnail size to fit the max size + //we do not compare to the image dimensions, but the thumbs + if (scope.maxSize) { + var ratioCalculation = cropperHelper.calculateAspectRatioFit(scope.width, scope.height, scope.maxSize, scope.maxSize, false); + //so if we have a max size, override the thumb sizes + scope.width = ratioCalculation.width; + scope.height = ratioCalculation.height; + } + setPreviewStyle(); + scope.loaded = true; + }); + }); + /// WATCHERS //// + scope.$watchCollection('[crop, center]', function (newValues, oldValues) { + //we have to reinit the whole thing if + //one of the external params changes + setPreviewStyle(); + }); + scope.$watch('center', function () { + setPreviewStyle(); + }, true); + function setPreviewStyle() { + if (scope.crop && scope.image) { + scope.preview = cropperHelper.convertToStyle(scope.crop, scope.image, { + width: scope.width, + height: scope.height + }, 0); + } else if (scope.image) { + //returns size fitting the cropper + var p = cropperHelper.calculateAspectRatioFit(scope.image.width, scope.image.height, scope.width, scope.height, true); + if (scope.center) { + var xy = cropperHelper.alignToCoordinates(p, scope.center, { + width: scope.width, + height: scope.height }); - }else{ - stylesheets.push("views/propertyeditors/grid/config/grid.default.rtestyles.css"); - styleFormats = fallbackStyles; + p.top = xy.top; + p.left = xy.left; + } else { + } + p.position = 'absolute'; + scope.preview = p; + } + } + } + }; + }); + angular.module('umbraco.directives') /** + * @ngdoc directive + * @name umbraco.directives.directive:localize + * @restrict EA + * @function + * @description + *
    + * Component
    + * Localize a specific token to put into the HTML as an item + *
    + *
    + * Attribute
    + * Add a HTML attribute to an element containing the HTML attribute name you wish to localise + * Using the format of '@section_key' or 'section_key' + *
    + * ##Usage + *
    +    * 
    +    * Close
    +    * Fallback value
    +    *
    +    * 
    +    * 
    +    * 
    +    * 
    + *
    + **/.directive('localize', function ($log, localizationService) { + return { + restrict: 'E', + scope: { key: '@' }, + replace: true, + link: function (scope, element, attrs) { + var key = scope.key; + localizationService.localize(key).then(function (value) { + element.html(value); + }); + } + }; + }).directive('localize', function ($log, localizationService) { + return { + restrict: 'A', + link: function (scope, element, attrs) { + //Support one or more attribute properties to update + var keys = attrs.localize.split(','); + angular.forEach(keys, function (value, key) { + var attr = element.attr(value); + if (attr) { + if (attr[0] === '@') { + //If the translation key starts with @ then remove it + attr = attr.substring(1); + } + var t = localizationService.tokenize(attr, scope); + localizationService.localize(t.key, t.tokens).then(function (val) { + element.attr(value, val); + }); + } + }); + } + }; + }); + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbNotifications + */ + (function () { + 'use strict'; + function NotificationDirective(notificationsService) { + function link(scope, el, attr, ctrl) { + //subscribes to notifications in the notification service + scope.notifications = notificationsService.current; + scope.$watch('notificationsService.current', function (newVal, oldVal, scope) { + if (newVal) { + scope.notifications = newVal; + } + }); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/notifications/umb-notifications.html', + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbNotifications', NotificationDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbOverlay +@restrict E +@scope + +@description + +

    Markup example

    +
    +    
    + + + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +
    +            vm.openOverlay = openOverlay;
    +
    +            function openOverlay() {
    +
    +                vm.overlay = {
    +                    view: "mediapicker",
    +                    show: true,
    +                    submit: function(model) {
    +
    +                        vm.overlay.show = false;
    +                        vm.overlay = null;
    +                    },
    +                    close: function(oldModel) {
    +                        vm.overlay.show = false;
    +                        vm.overlay = null;
    +                    }
    +                }
    +
    +            };
    +
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +    })();
    +
    + +

    General Options

    +Lorem ipsum dolor sit amet.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParamTypeDetails
    model.titleStringSet the title of the overlay.
    model.subTitleStringSet the subtitle of the overlay.
    model.submitButtonLabelStringSet an alternate submit button text
    model.submitButtonLabelKeyStringSet an alternate submit button label key for localized texts
    model.hideSubmitButtonBooleanHides the submit button
    model.closeButtonLabelStringSet an alternate close button text
    model.closeButtonLabelKeyStringSet an alternate close button label key for localized texts
    model.showBooleanShow/hide the overlay
    model.submitFunctionCallback function when the overlay submits. Returns the overlay model object
    model.closeFunctionCallback function when the overlay closes. Returns a copy of the overlay model object before being modified
    + + +

    Content Picker

    +Opens a content picker.
    +view: contentpicker + + + + + + + + + + + + + +
    ParamTypeDetails
    model.multiPickerBooleanPick one or multiple items
    + + + + + + + + + + + + + +
    ReturnsTypeDetails
    model.selectionArrayArray of content objects
    + + +

    Icon Picker

    +Opens an icon picker.
    +view: iconpicker + + + + + + + + + + + + + +
    ReturnsTypeDetails
    model.iconStringThe icon class
    + +

    Item Picker

    +Opens an item picker.
    +view: itempicker + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParamTypeDetails
    model.availableItemsArrayArray of available items
    model.selectedItemsArrayArray of selected items. When passed in the selected items will be filtered from the available items.
    model.filterBooleanSet to false to hide the filter
    + + + + + + + + + + + + + +
    ReturnsTypeDetails
    model.selectedItemObjectThe selected item
    + +

    Macro Picker

    +Opens a media picker.
    +view: macropicker + + + + + + + + + + + + + + + +
    ParamTypeDetails
    model.dialogDataObjectObject which contains array of allowedMacros. Set to null to allow all.
    + + + + + + + + + + + + + + + + + + + + +
    ReturnsTypeDetails
    model.macroParamsArrayArray of macro params
    model.selectedMacroObjectThe selected macro
    + +

    Media Picker

    +Opens a media picker.
    +view: mediapicker + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParamTypeDetails
    model.multiPickerBooleanPick one or multiple items
    model.onlyImagesBooleanOnly display files that have an image file-extension
    model.disableFolderSelectBooleanDisable folder selection
    + + + + + + + + + + + + + + + +
    ReturnsTypeDetails
    model.selectedImagesArrayArray of selected images
    + +

    Member Group Picker

    +Opens a member group picker.
    +view: membergrouppicker + + + + + + + + + + + + + + + +
    ParamTypeDetails
    model.multiPickerBooleanPick one or multiple items
    + + + + + + + + + + + + + + + + + + + + +
    ReturnsTypeDetails
    model.selectedMemberGroupStringThe selected member group
    model.selectedMemberGroups (multiPicker)ArrayThe selected member groups
    + +

    Member Picker

    +Opens a member picker.
    +view: memberpicker + + + + + + + + + + + + + + + +
    ParamTypeDetails
    model.multiPickerBooleanPick one or multiple items
    + + + + + + + + + + + + + + +
    ReturnsTypeDetails
    model.selectionArrayArray of selected members/td> +
    + +

    YSOD

    +Opens an overlay to show a custom YSOD.
    +view: ysod + + + + + + + + + + + + + + + +
    ParamTypeDetails
    model.errorObjectError object
    + +@param {object} model Overlay options. +@param {string} view Path to view or one of the default view names. +@param {string} position The overlay position ("left", "right", "center": "target"). +**/ + (function () { + 'use strict'; + function OverlayDirective($timeout, formHelper, overlayHelper, localizationService) { + function link(scope, el, attr, ctrl) { + scope.directive = { enableConfirmButton: false }; + var overlayNumber = 0; + var numberOfOverlays = 0; + var isRegistered = false; + var modelCopy = {}; + function activate() { + setView(); + setButtonText(); + modelCopy = makeModelCopy(scope.model); + $timeout(function () { + if (scope.position === 'target') { + setTargetPosition(); + } + // this has to be done inside a timeout to ensure the destroy + // event on other overlays is run before registering a new one + registerOverlay(); + setOverlayIndent(); + }); + } + function setView() { + if (scope.view) { + if (scope.view.indexOf('.html') === -1) { + var viewAlias = scope.view.toLowerCase(); + scope.view = 'views/common/overlays/' + viewAlias + '/' + viewAlias + '.html'; + } + } + } + function setButtonText() { + if (!scope.model.closeButtonLabelKey && !scope.model.closeButtonLabel) { + scope.model.closeButtonLabel = localizationService.localize('general_close'); + } + if (!scope.model.submitButtonLabelKey && !scope.model.submitButtonLabel) { + scope.model.submitButtonLabel = localizationService.localize('general_submit'); + } + } + function registerOverlay() { + overlayNumber = overlayHelper.registerOverlay(); + $(document).bind('keydown.overlay-' + overlayNumber, function (event) { + if (event.which === 27) { + numberOfOverlays = overlayHelper.getNumberOfOverlays(); + if (numberOfOverlays === overlayNumber) { + scope.$apply(function () { + scope.closeOverLay(); + }); + } + event.preventDefault(); + } + if (event.which === 13) { + numberOfOverlays = overlayHelper.getNumberOfOverlays(); + if (numberOfOverlays === overlayNumber) { + var activeElementType = document.activeElement.tagName; + var clickableElements = [ + 'A', + 'BUTTON' + ]; + var submitOnEnter = document.activeElement.hasAttribute('overlay-submit-on-enter'); + if (clickableElements.indexOf(activeElementType) === 0) { + document.activeElement.click(); + event.preventDefault(); + } else if (activeElementType === 'TEXTAREA' && !submitOnEnter) { + } else { + scope.$apply(function () { + scope.submitForm(scope.model); + }); + event.preventDefault(); + } + } + } + }); + isRegistered = true; + } + function unregisterOverlay() { + if (isRegistered) { + overlayHelper.unregisterOverlay(); + $(document).unbind('keydown.overlay-' + overlayNumber); + isRegistered = false; + } + } + function makeModelCopy(object) { + var newObject = {}; + for (var key in object) { + if (key !== 'event') { + newObject[key] = angular.copy(object[key]); + } + } + return newObject; + } + function setOverlayIndent() { + var overlayIndex = overlayNumber - 1; + var indentSize = overlayIndex * 20; + var overlayWidth = el.context.clientWidth; + el.css('width', overlayWidth - indentSize); + if (scope.position === 'center' || scope.position === 'target') { + var overlayTopPosition = el.context.offsetTop; + el.css('top', overlayTopPosition + indentSize); + } + } + function setTargetPosition() { + var container = $('#contentwrapper'); + var containerLeft = container[0].offsetLeft; + var containerRight = containerLeft + container[0].offsetWidth; + var containerTop = container[0].offsetTop; + var containerBottom = containerTop + container[0].offsetHeight; + var mousePositionClickX = null; + var mousePositionClickY = null; + var elementHeight = null; + var elementWidth = null; + var position = { + right: 'inherit', + left: 'inherit', + top: 'inherit', + bottom: 'inherit' + }; + // if mouse click position is know place element with mouse in center + if (scope.model.event && scope.model.event) { + // click position + mousePositionClickX = scope.model.event.pageX; + mousePositionClickY = scope.model.event.pageY; + // element size + elementHeight = el.context.clientHeight; + elementWidth = el.context.clientWidth; + // move element to this position + position.left = mousePositionClickX - elementWidth / 2; + position.top = mousePositionClickY - elementHeight / 2; + // check to see if element is outside screen + // outside right + if (position.left + elementWidth > containerRight) { + position.right = 10; + position.left = 'inherit'; + } + // outside bottom + if (position.top + elementHeight > containerBottom) { + position.bottom = 10; + position.top = 'inherit'; + } + // outside left + if (position.left < containerLeft) { + position.left = containerLeft + 10; + position.right = 'inherit'; + } + // outside top + if (position.top < containerTop) { + position.top = 10; + position.bottom = 'inherit'; + } + el.css(position); + } + } + scope.submitForm = function (model) { + if (scope.model.submit) { + if (formHelper.submitForm({ scope: scope })) { + formHelper.resetForm({ scope: scope }); + if (scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { + scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); + } else { + unregisterOverlay(); + scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); + } + } + } + }; + scope.cancelConfirmSubmit = function () { + scope.model.confirmSubmit.show = false; + }; + scope.closeOverLay = function () { + unregisterOverlay(); + if (scope.model.close) { + scope.model = modelCopy; + scope.model.close(scope.model); + } else { + scope.model.show = false; + scope.model = null; + } + }; + // angular does not support ng-show on custom directives + // width isolated scopes. So we have to make our own. + if (attr.hasOwnProperty('ngShow')) { + scope.$watch('ngShow', function (value) { + if (value) { + el.show(); + activate(); + } else { + unregisterOverlay(); + el.hide(); + } + }); + } else { + activate(); + } + scope.$on('$destroy', function () { + unregisterOverlay(); + }); + } + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/overlays/umb-overlay.html', + scope: { + ngShow: '=', + model: '=', + view: '=', + position: '@' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbOverlay', OverlayDirective); + }()); + (function () { + 'use strict'; + function OverlayBackdropDirective(overlayHelper) { + function link(scope, el, attr, ctrl) { + scope.numberOfOverlays = 0; + scope.$watch(function () { + return overlayHelper.getNumberOfOverlays(); + }, function (newValue) { + scope.numberOfOverlays = newValue; + }); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/overlays/umb-overlay-backdrop.html', + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbOverlayBackdrop', OverlayBackdropDirective); + }()); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbProperty +* @restrict E +**/ + angular.module('umbraco.directives').directive('umbProperty', function (umbPropEditorHelper) { + return { + scope: { property: '=' }, + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/property/umb-property.html', + link: function (scope) { + scope.propertyAlias = Umbraco.Sys.ServerVariables.isDebuggingEnabled === true ? scope.property.alias : null; + }, + //Define a controller for this directive to expose APIs to other directives + controller: function ($scope, $timeout) { + var self = this; + //set the API properties/methods + self.property = $scope.property; + self.setPropertyError = function (errorMsg) { + $scope.property.propertyErrorMessage = errorMsg; + }; + } + }; + }); + /** +* @ngdoc directive +* @function +* @name umbraco.directives.directive:umbPropertyEditor +* @requires formController +* @restrict E +**/ + //share property editor directive function + var _umbPropertyEditor = function (umbPropEditorHelper) { + return { + scope: { + model: '=', + isPreValue: '@', + preview: '@' + }, + require: '^form', + restrict: 'E', + replace: true, + templateUrl: 'views/components/property/umb-property-editor.html', + link: function (scope, element, attrs, ctrl) { + //we need to copy the form controller val to our isolated scope so that + //it get's carried down to the child scopes of this! + //we'll also maintain the current form name. + scope[ctrl.$name] = ctrl; + if (!scope.model.alias) { + scope.model.alias = Math.random().toString(36).slice(2); + } + scope.$watch('model.view', function (val) { + scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue); + }); + } + }; + }; + //Preffered is the umb-property-editor as its more explicit - but we keep umb-editor for backwards compat + angular.module('umbraco.directives').directive('umbPropertyEditor', _umbPropertyEditor); + angular.module('umbraco.directives').directive('umbEditor', _umbPropertyEditor); + angular.module('umbraco.directives.html').directive('umbPropertyGroup', function () { + return { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/property/umb-property-group.html' + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbTab +* @restrict E +**/ + angular.module('umbraco.directives').directive('umbTab', function ($parse, $timeout) { + return { + restrict: 'E', + replace: true, + transclude: 'true', + templateUrl: 'views/components/tabs/umb-tab.html' + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbTabs +* @restrict A +* @description Used to bind to bootstrap tab events so that sub directives can use this API to listen to tab changes +**/ + angular.module('umbraco.directives').directive('umbTabs', function () { + return { + restrict: 'A', + controller: function ($scope, $element, $attrs) { + var callbacks = []; + this.onTabShown = function (cb) { + callbacks.push(cb); + }; + function tabShown(event) { + var curr = $(event.target); + // active tab + var prev = $(event.relatedTarget); + // previous tab + $scope.$apply(); + for (var c in callbacks) { + callbacks[c].apply(this, [{ + current: curr, + previous: prev + }]); + } + } + //NOTE: it MUST be done this way - binding to an ancestor element that exists + // in the DOM to bind to the dynamic elements that will be created. + // It would be nicer to create this event handler as a directive for which child + // directives can attach to. + $element.on('shown', '.nav-tabs a', tabShown); + //ensure to unregister + $scope.$on('$destroy', function () { + $element.off('shown', '.nav-tabs a', tabShown); + for (var c in callbacks) { + delete callbacks[c]; + } + callbacks = null; + }); + } + }; + }); + (function () { + 'use strict'; + function UmbTabsContentDirective() { + function link(scope, el, attr, ctrl) { + scope.view = attr.view; + } + var directive = { + restrict: 'E', + replace: true, + transclude: 'true', + templateUrl: 'views/components/tabs/umb-tabs-content.html', + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbTabsContent', UmbTabsContentDirective); + }()); + (function () { + 'use strict'; + function UmbTabsNavDirective($timeout) { + function link(scope, el, attr) { + function activate() { + $timeout(function () { + //use bootstrap tabs API to show the first one + el.find('a:first').tab('show'); + //enable the tab drop + el.tabdrop(); + }); + } + var unbindModelWatch = scope.$watch('model', function (newValue, oldValue) { + activate(); + }); + scope.$on('$destroy', function () { + //ensure to destroy tabdrop (unbinds window resize listeners) + el.tabdrop('destroy'); + unbindModelWatch(); + }); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/tabs/umb-tabs-nav.html', + scope: { + model: '=', + tabdrop: '=', + idSuffix: '@' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbTabsNav', UmbTabsNavDirective); + }()); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbTree +* @restrict E +**/ + function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificationsService, $timeout, userService) { + return { + restrict: 'E', + replace: true, + terminal: false, + scope: { + section: '@', + treealias: '@', + hideoptions: '@', + hideheader: '@', + cachekey: '@', + isdialog: '@', + onlyinitialized: '@', + //Custom query string arguments to pass in to the tree as a string, example: "startnodeid=123&something=value" + customtreeparams: '@', + eventhandler: '=', + enablecheckboxes: '@', + enablelistviewsearch: '@', + enablelistviewexpand: '@' + }, + compile: function (element, attrs) { + //config + //var showheader = (attrs.showheader !== 'false'); + var hideoptions = attrs.hideoptions === 'true' ? 'hide-options' : ''; + var template = '
    • '; + template += '
      ' + '
      ' + ' {{tree.name}}
      ' + '' + '
      '; + template += '
        ' + '' + '
      ' + '
    • ' + '
    '; + element.replaceWith(template); + return function (scope, elem, attr, controller) { + //flag to track the last loaded section when the tree 'un-loads'. We use this to determine if we should + // re-load the tree again. For example, if we hover over 'content' the content tree is shown. Then we hover + // outside of the tree and the tree 'un-loads'. When we re-hover over 'content', we don't want to re-load the + // entire tree again since we already still have it in memory. Of course if the section is different we will + // reload it. This saves a lot on processing if someone is navigating in and out of the same section many times + // since it saves on data retreival and DOM processing. + var lastSection = ''; + //setup a default internal handler + if (!scope.eventhandler) { + scope.eventhandler = $({}); + } + //flag to enable/disable delete animations + var deleteAnimations = false; + /** Helper function to emit tree events */ + function emitEvent(eventName, args) { + if (scope.eventhandler) { + $(scope.eventhandler).trigger(eventName, args); } - - //stores a reference to the editor - var tinyMceEditor = null; - $q.all(await).then(function () { - - var uniqueId = scope.uniqueId; - - //create a baseline Config to exten upon - var baseLineConfigObj = { - mode: "exact", - skin: "umbraco", - plugins: plugins, - valid_elements: validElements, - invalid_elements: invalidElements, - extended_valid_elements: extendedValidElements, - menubar: false, - statusbar: false, - relative_urls: false, - toolbar: toolbar, - content_css: stylesheets, - style_formats: styleFormats, - autoresize_bottom_margin: 0 + } + /** This will deleteAnimations to true after the current digest */ + function enableDeleteAnimations() { + //do timeout so that it re-enables them after this digest + $timeout(function () { + //enable delete animations + deleteAnimations = true; + }, 0, false); + } + /*this is the only external interface a tree has */ + function setupExternalEvents() { + if (scope.eventhandler) { + scope.eventhandler.clearCache = function (section) { + treeService.clearCache({ section: section }); }; - - - if (tinyMceConfig.customConfig) { - - //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to - // convert it to json instead of having it as a string since this is what tinymce requires - for (var i in tinyMceConfig.customConfig) { - var val = tinyMceConfig.customConfig[i]; - if (val) { - val = val.toString().trim(); - if (val.detectIsJson()) { - try { - tinyMceConfig.customConfig[i] = JSON.parse(val); - //now we need to check if this custom config key is defined in our baseline, if it is we don't want to - //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise - //if it's an object it will overwrite the baseline - if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { - //concat it and below this concat'd array will overwrite the baseline in angular.extend - tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]); - } - } - catch (e) { - //cannot parse, we'll just leave it - } - } - } + scope.eventhandler.load = function (section) { + scope.section = section; + loadTree(); + }; + scope.eventhandler.reloadNode = function (node) { + if (!node) { + node = scope.currentNode; } - - angular.extend(baseLineConfigObj, tinyMceConfig.customConfig); - } - - //set all the things that user configs should not be able to override - baseLineConfigObj.elements = uniqueId; - baseLineConfigObj.setup = function (editor) { - - //set the reference - tinyMceEditor = editor; - - - //enable browser based spell checking - editor.on('init', function (e) { - - editor.getBody().setAttribute('spellcheck', true); - - //force overflow to hidden to prevent no needed scroll - editor.getBody().style.overflow = "hidden"; - - $timeout(function(){ - if(scope.value === null){ - editor.focus(); - } - }, 400); - - }); - - // pin toolbar to top of screen if we have focus and it scrolls off the screen - var pinToolbar = function () { - - var _toolbar = $(editor.editorContainer).find(".mce-toolbar"); - var toolbarHeight = _toolbar.height(); - - var _tinyMce = $(editor.editorContainer); - var tinyMceRect = _tinyMce[0].getBoundingClientRect(); - var tinyMceTop = tinyMceRect.top; - var tinyMceBottom = tinyMceRect.bottom; - var tinyMceWidth = tinyMceRect.width; - - var _tinyMceEditArea = _tinyMce.find(".mce-edit-area"); - - // set padding in top of mce so the content does not "jump" up - _tinyMceEditArea.css("padding-top", toolbarHeight); - - if (tinyMceTop < 160 && ((160 + toolbarHeight) < tinyMceBottom)) { - _toolbar - .css("visibility", "visible") - .css("position", "fixed") - .css("top", "160px") - .css("margin-top", "0") - .css("width", tinyMceWidth); - } else { - _toolbar - .css("visibility", "visible") - .css("position", "absolute") - .css("top", "auto") - .css("margin-top", "0") - .css("width", tinyMceWidth); - } - - }; - - // unpin toolbar to top of screen - var unpinToolbar = function() { - - var _toolbar = $(editor.editorContainer).find(".mce-toolbar"); - var _tinyMce = $(editor.editorContainer); - var _tinyMceEditArea = _tinyMce.find(".mce-edit-area"); - - // reset padding in top of mce so the content does not "jump" up - _tinyMceEditArea.css("padding-top", "0"); - - _toolbar.css("position", "static"); - - }; - - //when we leave the editor (maybe) - editor.on('blur', function (e) { - editor.save(); - angularHelper.safeApply(scope, function () { - scope.value = editor.getContent(); - - var _toolbar = $(editor.editorContainer) - .find(".mce-toolbar"); - - if(scope.onBlur){ - scope.onBlur(); - } - - unpinToolbar(); - $('.umb-panel-body').off('scroll', pinToolbar); - - }); - }); - - // Focus on editor - editor.on('focus', function (e) { - angularHelper.safeApply(scope, function () { - - if(scope.onFocus){ - scope.onFocus(); - } - - pinToolbar(); - $('.umb-panel-body').on('scroll', pinToolbar); - - }); - }); - - // Click on editor - editor.on('click', function (e) { - angularHelper.safeApply(scope, function () { - - if(scope.onClick){ - scope.onClick(); - } - - pinToolbar(); - $('.umb-panel-body').on('scroll', pinToolbar); - - }); - }); - - //when buttons modify content - editor.on('ExecCommand', function (e) { - editor.save(); - angularHelper.safeApply(scope, function () { - scope.value = editor.getContent(); - }); - }); - - // Update model on keypress - editor.on('KeyUp', function (e) { - editor.save(); - angularHelper.safeApply(scope, function () { - scope.value = editor.getContent(); - }); - }); - - // Update model on change, i.e. copy/pasted text, plugins altering content - editor.on('SetContent', function (e) { - if (!e.initial) { - editor.save(); - angularHelper.safeApply(scope, function () { - scope.value = editor.getContent(); - }); - } - }); - - editor.on('ObjectResized', function (e) { - var qs = "?width=" + e.width + "&height=" + e.height; - var srcAttr = $(e.target).attr("src"); - var path = srcAttr.split("?")[0]; - $(e.target).attr("data-mce-src", path + qs); + if (node) { + scope.loadChildren(node, true); + } + }; + /** + Used to do the tree syncing. If the args.tree is not specified we are assuming it has been + specified previously using the _setActiveTreeType + */ + scope.eventhandler.syncTree = function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.path) { + throw 'args.path cannot be null'; + } + var deferred = $q.defer(); + //this is super complex but seems to be working in other places, here we're listening for our + // own events, once the tree is sycned we'll resolve our promise. + scope.eventhandler.one('treeSynced', function (e, syncArgs) { + deferred.resolve(syncArgs); }); - - //Create the insert link plugin - tinyMceService.createLinkPicker(editor, scope, function(currentTarget, anchorElement){ - if(scope.onLinkPickerClick) { - scope.onLinkPickerClick(editor, currentTarget, anchorElement); - } + //this should normally be set unless it is being called from legacy + // code, so set the active tree type before proceeding. + if (args.tree) { + loadActiveTree(args.tree); + } + if (angular.isString(args.path)) { + args.path = args.path.replace('"', '').split(','); + } + //reset current node selection + //scope.currentNode = null; + //Filter the path for root node ids (we don't want to pass in -1 or 'init') + args.path = _.filter(args.path, function (item) { + return item !== 'init' && item !== '-1'; }); - - //Create the insert media plugin - tinyMceService.createMediaPicker(editor, scope, function(currentTarget, userData){ - if(scope.onMediaPickerClick) { - scope.onMediaPickerClick(editor, currentTarget, userData); + //Once those are filtered we need to check if the current user has a special start node id, + // if they do, then we're going to trim the start of the array for anything found from that start node + // and previous so that the tree syncs properly. The tree syncs from the top down and if there are parts + // of the tree's path in there that don't actually exist in the dom/model then syncing will not work. + userService.getCurrentUser().then(function (userData) { + var startNodes = []; + for (var i = 0; i < userData.startContentIds; i++) { + startNodes.push(userData.startContentIds[i]); } - }); - - //Create the embedded plugin - tinyMceService.createInsertEmbeddedMedia(editor, scope, function(){ - if(scope.onEmbedClick) { - scope.onEmbedClick(editor); + for (var j = 0; j < userData.startMediaIds; j++) { + startNodes.push(userData.startMediaIds[j]); } + _.each(startNodes, function (i) { + var found = _.find(args.path, function (p) { + return String(p) === String(i); + }); + if (found) { + args.path = args.path.splice(_.indexOf(args.path, found)); + } + }); + loadPath(args.path, args.forceReload, args.activate); }); - - //Create the insert macro plugin - tinyMceService.createInsertMacro(editor, scope, function(dialogData){ - if(scope.onMacroPickerClick) { - scope.onMacroPickerClick(editor, dialogData); - } + return deferred.promise; + }; + /** + Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to + have to set an active tree and then sync, the new API does this in one method by using syncTree. + loadChildren is optional but if it is set, it will set the current active tree and load the root + node's children - this is synonymous with the legacy refreshTree method - again should not be used + and should only be used for the legacy code to work. + */ + scope.eventhandler._setActiveTreeType = function (treeAlias, loadChildren) { + loadActiveTree(treeAlias, loadChildren); + }; + } + } + //helper to load a specific path on the active tree as soon as its ready + function loadPath(path, forceReload, activate) { + if (scope.activeTree) { + syncTree(scope.activeTree, path, forceReload, activate); + } else { + scope.eventhandler.one('activeTreeLoaded', function (e, args) { + syncTree(args.tree, path, forceReload, activate); + }); + } + } + //given a tree alias, this will search the current section tree for the specified tree alias and + //set that to the activeTree + //NOTE: loadChildren is ONLY used for legacy purposes, do not use this when syncing the tree as it will cause problems + // since there will be double request and event handling operations. + function loadActiveTree(treeAlias, loadChildren) { + if (!treeAlias) { + return; + } + scope.activeTree = undefined; + function doLoad(tree) { + var childrenAndSelf = [tree].concat(tree.children); + scope.activeTree = _.find(childrenAndSelf, function (node) { + if (node && node.metaData && node.metaData.treeAlias) { + return node.metaData.treeAlias.toUpperCase() === treeAlias.toUpperCase(); + } + return false; + }); + if (!scope.activeTree) { + throw 'Could not find the tree ' + treeAlias + ', activeTree has not been set'; + } + //This is only used for the legacy tree method refreshTree! + if (loadChildren) { + scope.activeTree.expanded = true; + scope.loadChildren(scope.activeTree, false).then(function () { + emitEvent('activeTreeLoaded', { tree: scope.activeTree }); }); - + } else { + emitEvent('activeTreeLoaded', { tree: scope.activeTree }); + } + } + if (scope.tree) { + doLoad(scope.tree.root); + } else { + scope.eventhandler.one('treeLoaded', function (e, args) { + doLoad(args.tree.root); + }); + } + } + /** Method to load in the tree data */ + function loadTree() { + if (!scope.loading && scope.section) { + scope.loading = true; + //anytime we want to load the tree we need to disable the delete animations + deleteAnimations = false; + //default args + var args = { + section: scope.section, + tree: scope.treealias, + cacheKey: scope.cachekey, + isDialog: scope.isdialog ? scope.isdialog : false, + onlyinitialized: scope.onlyinitialized }; - - /** Loads in the editor */ - function loadTinyMce() { - - //we need to add a timeout here, to force a redraw so TinyMCE can find - //the elements needed - $timeout(function () { - tinymce.DOM.events.domLoaded = true; - tinymce.init(baseLineConfigObj); - }, 150, false); + //add the extra query string params if specified + if (scope.customtreeparams) { + args['queryString'] = scope.customtreeparams; } - - loadTinyMce(); - - //here we declare a special method which will be called whenever the value has changed from the server - //this is instead of doing a watch on the model.value = faster - //scope.model.onValueChanged = function (newVal, oldVal) { - // //update the display val again if it has changed from the server; - // tinyMceEditor.setContent(newVal, { format: 'raw' }); - // //we need to manually fire this event since it is only ever fired based on loading from the DOM, this - // // is required for our plugins listening to this event to execute - // tinyMceEditor.fire('LoadContent', null); - //}; - - //listen for formSubmitting event (the result is callback used to remove the event subscription) - var unsubscribe = scope.$on("formSubmitting", function () { - //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer - // we do parse it out on the server side but would be nice to do that on the client side before as well. - scope.value = tinyMceEditor.getContent(); + treeService.getTree(args).then(function (data) { + //set the data once we have it + scope.tree = data; + enableDeleteAnimations(); + scope.loading = false; + //set the root as the current active tree + scope.activeTree = scope.tree.root; + emitEvent('treeLoaded', { tree: scope.tree }); + emitEvent('treeNodeExpanded', { + tree: scope.tree, + node: scope.tree.root, + children: scope.tree.root.children + }); + }, function (reason) { + scope.loading = false; + notificationsService.error('Tree Error', reason); }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom - // element might still be there even after the modal has been hidden. - scope.$on('$destroy', function () { - unsubscribe(); + } + } + /** syncs the tree, the treeNode can be ANY tree node in the tree that requires syncing */ + function syncTree(treeNode, path, forceReload, activate) { + deleteAnimations = false; + treeService.syncTree({ + node: treeNode, + path: path, + forceReload: forceReload + }).then(function (data) { + if (activate === undefined || activate === true) { + scope.currentNode = data; + } + emitEvent('treeSynced', { + node: data, + activate: activate + }); + enableDeleteAnimations(); + }); + } + /** Returns the css classses assigned to the node (div element) */ + scope.getNodeCssClass = function (node) { + if (!node) { + return ''; + } + //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time + // it would be better if we could cache the processing. The problem is that some of these things are dynamic. + var css = []; + if (node.cssClasses) { + _.each(node.cssClasses, function (c) { + css.push(c); + }); + } + return css.join(' '); + }; + scope.selectEnabledNodeClass = function (node) { + return node ? node.selected ? 'icon umb-tree-icon sprTree icon-check green temporary' : '' : ''; + }; + /** method to set the current animation for the node. + * This changes dynamically based on if we are changing sections or just loading normal tree data. + * When changing sections we don't want all of the tree-ndoes to do their 'leave' animations. + */ + scope.animation = function () { + if (deleteAnimations && scope.tree && scope.tree.root && scope.tree.root.expanded) { + return { leave: 'tree-node-delete-leave' }; + } else { + return {}; + } + }; + /* helper to force reloading children of a tree node */ + scope.loadChildren = function (node, forceReload) { + var deferred = $q.defer(); + //emit treeNodeExpanding event, if a callback object is set on the tree + emitEvent('treeNodeExpanding', { + tree: scope.tree, + node: node + }); + //standardising + if (!node.children) { + node.children = []; + } + if (forceReload || node.hasChildren && node.children.length === 0) { + //get the children from the tree service + treeService.loadNodeChildren({ + node: node, + section: scope.section + }).then(function (data) { + //emit expanded event + emitEvent('treeNodeExpanded', { + tree: scope.tree, + node: node, + children: data + }); + enableDeleteAnimations(); + deferred.resolve(data); + }); + } else { + emitEvent('treeNodeExpanded', { + tree: scope.tree, + node: node, + children: node.children }); - + node.expanded = true; + enableDeleteAnimations(); + deferred.resolve(node.children); + } + return deferred.promise; + }; + /** + Method called when the options button next to the root node is called. + The tree doesnt know about this, so it raises an event to tell the parent controller + about it. + */ + scope.options = function (n, ev) { + emitEvent('treeOptionsClick', { + element: elem, + node: n, + event: ev + }); + }; + /** + Method called when an item is clicked in the tree, this passes the + DOM element, the tree node object and the original click + and emits it as a treeNodeSelect element if there is a callback object + defined on the tree + */ + scope.select = function (n, ev) { + if (n.metaData && n.metaData.noAccess === true) { + ev.preventDefault(); + return; + } + //on tree select we need to remove the current node - + // whoever handles this will need to make sure the correct node is selected + //reset current node selection + scope.currentNode = null; + emitEvent('treeNodeSelect', { + element: elem, + node: n, + event: ev + }); + }; + scope.altSelect = function (n, ev) { + emitEvent('treeNodeAltSelect', { + element: elem, + tree: scope.tree, + node: n, + event: ev }); - + }; + //watch for section changes + scope.$watch('section', function (newVal, oldVal) { + if (!scope.tree) { + loadTree(); + } + if (!newVal) { + //store the last section loaded + lastSection = oldVal; + } else if (newVal !== oldVal && newVal !== lastSection) { + //only reload the tree data and Dom if the newval is different from the old one + // and if the last section loaded is different from the requested one. + loadTree(); + //store the new section to be loaded as the last section + //clear any active trees to reset lookups + lastSection = newVal; + } }); - + setupExternalEvents(); + loadTree(); }; - - initTiny(); - } }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbControlGroup -* @restrict E -**/ -angular.module("umbraco.directives.html") - .directive('umbControlGroup', function (localizationService) { - return { - scope: { - label: "@label", - description: "@", - hideLabel: "@", - alias: "@" - }, - require: '?^form', - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/html/umb-control-group.html', - link: function (scope, element, attr, formCtrl) { - - scope.formValid = function() { - if (formCtrl) { - return formCtrl.$valid; - } - //there is no form. - return true; - }; - - if (scope.label && scope.label[0] === "@") { - scope.labelstring = localizationService.localize(scope.label.substring(1)); - } - else { - scope.labelstring = scope.label; - } - - if (scope.description && scope.description[0] === "@") { - scope.descriptionstring = localizationService.localize(scope.description.substring(1)); - } - else { - scope.descriptionstring = scope.description; - } - - } - }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbPane -* @restrict E -**/ -angular.module("umbraco.directives.html") - .directive('umbPane', function () { + } + angular.module('umbraco.directives').directive('umbTree', umbTreeDirective); + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbTreeItem + * @element li + * @function + * + * @description + * Renders a list item, representing a single node in the tree. + * Includes element to toggle children, and a menu toggling button + * + * **note:** This directive is only used internally in the umbTree directive + * + * @example + + + + + + */ + angular.module('umbraco.directives').directive('umbTreeItem', function ($compile, $http, $templateCache, $interpolate, $log, $location, $rootScope, $window, treeService, $timeout, localizationService) { return { - transclude: true, restrict: 'E', replace: true, - templateUrl: 'views/components/html/umb-pane.html' - }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbPanel -* @restrict E -**/ -angular.module("umbraco.directives.html") - .directive('umbPanel', function($timeout, $log){ - return { - restrict: 'E', - replace: true, - transclude: 'true', - templateUrl: 'views/components/html/umb-panel.html' - }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbImageCrop -* @restrict E -* @function -**/ -angular.module("umbraco.directives") - .directive('umbImageCrop', - function ($timeout, localizationService, cropperHelper, $log) { - return { - restrict: 'E', - replace: true, - templateUrl: 'views/components/imaging/umb-image-crop.html', - scope: { - src: '=', - width: '@', - height: '@', - crop: "=", - center: "=", - maxSize: '@' - }, - - link: function(scope, element, attrs) { - scope.width = 400; - scope.height = 320; - - scope.dimensions = { - image: {}, - cropper:{}, - viewport:{}, - margin: 20, - scale: { - min: 0.3, - max: 3, - current: 1 - } - }; - - - //live rendering of viewport and image styles - scope.style = function () { - return { - 'height': (parseInt(scope.dimensions.viewport.height, 10)) + 'px', - 'width': (parseInt(scope.dimensions.viewport.width, 10)) + 'px' - }; - }; - - - //elements - var $viewport = element.find(".viewport"); - var $image = element.find("img"); - var $overlay = element.find(".overlay"); - var $container = element.find(".crop-container"); - - //default constraints for drag n drop - var constraints = {left: {max: scope.dimensions.margin, min: scope.dimensions.margin}, top: {max: scope.dimensions.margin, min: scope.dimensions.margin}, }; - scope.constraints = constraints; - - - //set constaints for cropping drag and drop - var setConstraints = function(){ - constraints.left.min = scope.dimensions.margin + scope.dimensions.cropper.width - scope.dimensions.image.width; - constraints.top.min = scope.dimensions.margin + scope.dimensions.cropper.height - scope.dimensions.image.height; - }; - - - var setDimensions = function(originalImage){ - originalImage.width("auto"); - originalImage.height("auto"); - - var image = {}; - image.originalWidth = originalImage.width(); - image.originalHeight = originalImage.height(); - - image.width = image.originalWidth; - image.height = image.originalHeight; - image.left = originalImage[0].offsetLeft; - image.top = originalImage[0].offsetTop; - - scope.dimensions.image = image; - - //unscaled editor size - //var viewPortW = $viewport.width(); - //var viewPortH = $viewport.height(); - var _viewPortW = parseInt(scope.width, 10); - var _viewPortH = parseInt(scope.height, 10); - - //if we set a constraint we will scale it down if needed - if(scope.maxSize){ - var ratioCalculation = cropperHelper.scaleToMaxSize( - _viewPortW, - _viewPortH, - scope.maxSize); - - //so if we have a max size, override the thumb sizes - _viewPortW = ratioCalculation.width; - _viewPortH = ratioCalculation.height; - } - - scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin; - scope.dimensions.viewport.height = _viewPortH + 2 * scope.dimensions.margin; - scope.dimensions.cropper.width = _viewPortW; // scope.dimensions.viewport.width - 2 * scope.dimensions.margin; - scope.dimensions.cropper.height = _viewPortH; // scope.dimensions.viewport.height - 2 * scope.dimensions.margin; - }; - - - //when loading an image without any crop info, we center and fit it - var resizeImageToEditor = function(){ - //returns size fitting the cropper - var size = cropperHelper.calculateAspectRatioFit( - scope.dimensions.image.width, - scope.dimensions.image.height, - scope.dimensions.cropper.width, - scope.dimensions.cropper.height, - true); - - //sets the image size and updates the scope - scope.dimensions.image.width = size.width; - scope.dimensions.image.height = size.height; - - //calculate the best suited ratios - scope.dimensions.scale.min = size.ratio; - scope.dimensions.scale.max = 2; - scope.dimensions.scale.current = size.ratio; - - //center the image - var position = cropperHelper.centerInsideViewPort(scope.dimensions.image, scope.dimensions.cropper); - scope.dimensions.top = position.top; - scope.dimensions.left = position.left; - - setConstraints(); - }; - - //resize to a given ratio - var resizeImageToScale = function(ratio){ - //do stuff - var size = cropperHelper.calculateSizeToRatio(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, ratio); - scope.dimensions.image.width = size.width; - scope.dimensions.image.height = size.height; - - setConstraints(); - validatePosition(scope.dimensions.image.left, scope.dimensions.image.top); - }; - - //resize the image to a predefined crop coordinate - var resizeImageToCrop = function(){ - scope.dimensions.image = cropperHelper.convertToStyle( - scope.crop, - {width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight}, - scope.dimensions.cropper, - scope.dimensions.margin); - - var ratioCalculation = cropperHelper.calculateAspectRatioFit( - scope.dimensions.image.originalWidth, - scope.dimensions.image.originalHeight, - scope.dimensions.cropper.width, - scope.dimensions.cropper.height, - true); - - scope.dimensions.scale.current = scope.dimensions.image.ratio; - - //min max based on original width/height - scope.dimensions.scale.min = ratioCalculation.ratio; - scope.dimensions.scale.max = 2; - }; - - - - var validatePosition = function(left, top){ - if(left > constraints.left.max) - { - left = constraints.left.max; - } - - if(left <= constraints.left.min){ - left = constraints.left.min; - } - - if(top > constraints.top.max) - { - top = constraints.top.max; - } - if(top <= constraints.top.min){ - top = constraints.top.min; - } - - if(scope.dimensions.image.left !== left){ - scope.dimensions.image.left = left; - } - - if(scope.dimensions.image.top !== top){ - scope.dimensions.image.top = top; - } - }; - - - //sets scope.crop to the recalculated % based crop - var calculateCropBox = function(){ - scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); - }; - - - //Drag and drop positioning, using jquery ui draggable - var onStartDragPosition, top, left; - $overlay.draggable({ - drag: function(event, ui) { - scope.$apply(function(){ - validatePosition(ui.position.left, ui.position.top); - }); - }, - stop: function(event, ui){ - scope.$apply(function(){ - //make sure that every validates one more time... - validatePosition(ui.position.left, ui.position.top); - - calculateCropBox(); - scope.dimensions.image.rnd = Math.random(); - }); - } - }); - - - - var init = function(image){ - scope.loaded = false; - - //set dimensions on image, viewport, cropper etc - setDimensions(image); - - //if we have a crop already position the image - if(scope.crop){ - resizeImageToCrop(); - }else{ - resizeImageToEditor(); - } - - //sets constaints for the cropper - setConstraints(); - scope.loaded = true; - }; - - - /// WATCHERS //// - scope.$watchCollection('[width, height]', function(newValues, oldValues){ - //we have to reinit the whole thing if - //one of the external params changes - if(newValues !== oldValues){ - setDimensions($image); - setConstraints(); - } - }); - - var throttledResizing = _.throttle(function(){ - resizeImageToScale(scope.dimensions.scale.current); - calculateCropBox(); - }, 100); - - - //happens when we change the scale - scope.$watch("dimensions.scale.current", function(){ - if(scope.loaded){ - throttledResizing(); - } - }); - - //ie hack - if(window.navigator.userAgent.indexOf("MSIE ")){ - var ranger = element.find("input"); - ranger.bind("change",function(){ - scope.$apply(function(){ - scope.dimensions.scale.current = ranger.val(); - }); - }); - } - - //// INIT ///// - $image.load(function(){ - $timeout(function(){ - init($image); - }); - }); - } - }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbImageGravity -* @restrict E -* @function -* @description -**/ -angular.module("umbraco.directives") - .directive('umbImageGravity', function ($timeout, localizationService, $log) { - return { - restrict: 'E', - replace: true, - templateUrl: 'views/components/imaging/umb-image-gravity.html', - scope: { - src: '=', - center: "=", - onImageLoaded: "=" - }, - link: function(scope, element, attrs) { - - //Internal values for keeping track of the dot and the size of the editor - scope.dimensions = { - width: 0, - height: 0, - left: 0, - top: 0 - }; - - scope.loaded = false; - - //elements - var $viewport = element.find(".viewport"); - var $image = element.find("img"); - var $overlay = element.find(".overlay"); - - scope.style = function () { - if(scope.dimensions.width <= 0){ - setDimensions(); - } - - return { - 'top': scope.dimensions.top + 'px', - 'left': scope.dimensions.left + 'px' - }; - }; - - scope.setFocalPoint = function(event) { - - scope.$emit("imageFocalPointStart"); - - var offsetX = event.offsetX - 10; - var offsetY = event.offsetY - 10; - - calculateGravity(offsetX, offsetY); - - lazyEndEvent(); - - }; - - var setDimensions = function(){ - scope.dimensions.width = $image.width(); - scope.dimensions.height = $image.height(); - - if(scope.center){ - scope.dimensions.left = scope.center.left * scope.dimensions.width -10; - scope.dimensions.top = scope.center.top * scope.dimensions.height -10; - }else{ - scope.center = { left: 0.5, top: 0.5 }; - } - }; - - var calculateGravity = function(offsetX, offsetY){ - scope.dimensions.left = offsetX; - scope.dimensions.top = offsetY; - - scope.center.left = (scope.dimensions.left+10) / scope.dimensions.width; - scope.center.top = (scope.dimensions.top+10) / scope.dimensions.height; - }; - - var lazyEndEvent = _.debounce(function(){ - scope.$apply(function(){ - scope.$emit("imageFocalPointStop"); - }); - }, 2000); - - - //Drag and drop positioning, using jquery ui draggable - //TODO ensure that the point doesnt go outside the box - $overlay.draggable({ - containment: "parent", - start: function(){ - scope.$apply(function(){ - scope.$emit("imageFocalPointStart"); - }); - }, - stop: function() { - scope.$apply(function(){ - var offsetX = $overlay[0].offsetLeft; - var offsetY = $overlay[0].offsetTop; - calculateGravity(offsetX, offsetY); - }); - - lazyEndEvent(); - } - }); - - //// INIT ///// - $image.load(function() { - $timeout(function() { - setDimensions(); - scope.loaded = true; - if (angular.isFunction(scope.onImageLoaded)) { - scope.onImageLoaded(); - } - }); - }); - - $(window).on('resize.umbImageGravity', function(){ - scope.$apply(function(){ - $timeout(function(){ - setDimensions(); - }); - // Make sure we can find the offset values for the overlay(dot) before calculating - // fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte) - if($overlay.is(':visible')) { - var offsetX = $overlay[0].offsetLeft; - var offsetY = $overlay[0].offsetTop; - calculateGravity(offsetX, offsetY); - } - }); - }); - - scope.$on('$destroy', function() { - $(window).off('.umbImageGravity'); - }); - - } - }; - }); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbImageThumbnail -* @restrict E -* @function -* @description -**/ -angular.module("umbraco.directives") - .directive('umbImageThumbnail', - function ($timeout, localizationService, cropperHelper, $log) { - return { - restrict: 'E', - replace: true, - templateUrl: 'views/components/imaging/umb-image-thumbnail.html', - - scope: { - src: '=', - width: '@', - height: '@', - center: "=", - crop: "=", - maxSize: '@' - }, - - link: function(scope, element, attrs) { - //// INIT ///// - var $image = element.find("img"); - scope.loaded = false; - - $image.load(function(){ - $timeout(function(){ - $image.width("auto"); - $image.height("auto"); - - scope.image = {}; - scope.image.width = $image[0].width; - scope.image.height = $image[0].height; - - //we force a lower thumbnail size to fit the max size - //we do not compare to the image dimensions, but the thumbs - if(scope.maxSize){ - var ratioCalculation = cropperHelper.calculateAspectRatioFit( - scope.width, - scope.height, - scope.maxSize, - scope.maxSize, - false); - - //so if we have a max size, override the thumb sizes - scope.width = ratioCalculation.width; - scope.height = ratioCalculation.height; - } - - setPreviewStyle(); - scope.loaded = true; - }); - }); - - /// WATCHERS //// - scope.$watchCollection('[crop, center]', function(newValues, oldValues){ - //we have to reinit the whole thing if - //one of the external params changes - setPreviewStyle(); - }); - - scope.$watch("center", function(){ - setPreviewStyle(); - }, true); - - function setPreviewStyle(){ - if(scope.crop && scope.image){ - scope.preview = cropperHelper.convertToStyle( - scope.crop, - scope.image, - {width: scope.width, height: scope.height}, - 0); - }else if(scope.image){ - - //returns size fitting the cropper - var p = cropperHelper.calculateAspectRatioFit( - scope.image.width, - scope.image.height, - scope.width, - scope.height, - true); - - - if(scope.center){ - var xy = cropperHelper.alignToCoordinates(p, scope.center, {width: scope.width, height: scope.height}); - p.top = xy.top; - p.left = xy.left; - }else{ - - } - - p.position = "absolute"; - scope.preview = p; - } - } - } - }; - }); - -angular.module("umbraco.directives") - - /** - * @ngdoc directive - * @name umbraco.directives.directive:localize - * @restrict EA - * @function - * @description - *
    - * Component
    - * Localize a specific token to put into the HTML as an item - *
    - *
    - * Attribute
    - * Add a HTML attribute to an element containing the HTML attribute name you wish to localise - * Using the format of '@section_key' or 'section_key' - *
    - * ##Usage - *
    -    * 
    -    * Close
    -    * Fallback value
    -    *
    -    * 
    -    * 
    -    * 
    -    * 
    - *
    - **/ - .directive('localize', function ($log, localizationService) { - return { - restrict: 'E', - scope:{ - key: '@' + scope: { + section: '@', + eventhandler: '=', + currentNode: '=', + enablelistviewexpand: '@', + node: '=', + tree: '=' }, - replace: true, - + //TODO: Remove more of the binding from this template and move the DOM manipulation to be manually done in the link function, + // this will greatly improve performance since there's potentially a lot of nodes being rendered = a LOT of watches! + template: '
  • ' + '
    ' + //NOTE: This ins element is used to display the search icon if the node is a container/listview and the tree is currently in dialog + //'' + + ' ' + '' + '' + //NOTE: These are the 'option' elipses + '' + '
    ' + '
    ' + '
  • ', link: function (scope, element, attrs) { - var key = scope.key; - localizationService.localize(key).then(function(value){ - element.html(value); + localizationService.localize('general_search').then(function (value) { + scope.searchAltText = value; }); - } - }; - }) - - .directive('localize', function ($log, localizationService) { - return { - restrict: 'A', - link: function (scope, element, attrs) { - //Support one or more attribute properties to update - var keys = attrs.localize.split(','); - - angular.forEach(keys, function(value, key){ - var attr = element.attr(value); - - if(attr){ - if(attr[0] === '@'){ - //If the translation key starts with @ then remove it - attr = attr.substring(1); - } - - var t = localizationService.tokenize(attr, scope); - - localizationService.localize(t.key, t.tokens).then(function(val){ - element.attr(value, val); - }); + //flag to enable/disable delete animations, default for an item is true + var deleteAnimations = true; + // Helper function to emit tree events + function emitEvent(eventName, args) { + if (scope.eventhandler) { + $(scope.eventhandler).trigger(eventName, args); } - }); - } - }; - - }); - -/** - * @ngdoc directive - * @name umbraco.directives.directive:umbNotifications - */ - -(function() { - 'use strict'; - - function NotificationDirective(notificationsService) { - - function link(scope, el, attr, ctrl) { - - //subscribes to notifications in the notification service - scope.notifications = notificationsService.current; - scope.$watch('notificationsService.current', function (newVal, oldVal, scope) { - if (newVal) { - scope.notifications = newVal; - } - }); - - } - - var directive = { - restrict: "E", - replace: true, - templateUrl: 'views/components/notifications/umb-notifications.html', - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbNotifications', NotificationDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbOverlay -@restrict E -@scope - -@description - -

    Markup example

    -
    -    
    - - - - - - -
    -
    - -

    Controller example

    -
    -    (function () {
    -
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -
    -            vm.openOverlay = openOverlay;
    -
    -            function openOverlay() {
    -
    -                vm.overlay = {
    -                    view: "mediapicker",
    -                    show: true,
    -                    submit: function(model) {
    -
    -                        vm.overlay.show = false;
    -                        vm.overlay = null;
    -                    },
    -                    close: function(oldModel) {
    -                        vm.overlay.show = false;
    -                        vm.overlay = null;
    +                }
    +                // updates the node's DOM/styles
    +                function setupNodeDom(node, tree) {
    +                    //get the first div element
    +                    element.children(':first')    //set the padding
    +.css('padding-left', node.level * 20 + 'px');
    +                    //toggle visibility of last 'ins' depending on children
    +                    //visibility still ensure the space is "reserved", so both nodes with and without children are aligned.
    +                    if (node.hasChildren || node.metaData.isContainer && scope.enablelistviewexpand === 'true') {
    +                        element.find('ins').last().css('visibility', 'visible');
    +                    } else {
    +                        element.find('ins').last().css('visibility', 'hidden');
    +                    }
    +                    var icon = element.find('i:first');
    +                    icon.addClass(node.cssClass);
    +                    icon.attr('title', node.routePath);
    +                    element.find('a:first').text(node.name);
    +                    if (!node.menuUrl) {
    +                        element.find('a.umb-options').remove();
    +                    }
    +                    if (node.style) {
    +                        element.find('i:first').attr('style', node.style);
                         }
                     }
    -
    -            };
    -
    -        }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    - -

    General Options

    -Lorem ipsum dolor sit amet.. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ParamTypeDetails
    model.titleStringSet the title of the overlay.
    model.subTitleStringSet the subtitle of the overlay.
    model.submitButtonLabelStringSet an alternate submit button text
    model.submitButtonLabelKeyStringSet an alternate submit button label key for localized texts
    model.hideSubmitButtonBooleanHides the submit button
    model.closeButtonLabelStringSet an alternate close button text
    model.closeButtonLabelKeyStringSet an alternate close button label key for localized texts
    model.showBooleanShow/hide the overlay
    model.submitFunctionCallback function when the overlay submits. Returns the overlay model object
    model.closeFunctionCallback function when the overlay closes. Returns a copy of the overlay model object before being modified
    - - -

    Content Picker

    -Opens a content picker.
    -view: contentpicker - - - - - - - - - - - - - -
    ParamTypeDetails
    model.multiPickerBooleanPick one or multiple items
    - - - - - - - - - - - - - -
    ReturnsTypeDetails
    model.selectionArrayArray of content objects
    - - -

    Icon Picker

    -Opens an icon picker.
    -view: iconpicker - - - - - - - - - - - - - -
    ReturnsTypeDetails
    model.iconStringThe icon class
    - -

    Item Picker

    -Opens an item picker.
    -view: itempicker - - - - - - - - - - - - - - - - - - - - - - - - - -
    ParamTypeDetails
    model.availableItemsArrayArray of available items
    model.selectedItemsArrayArray of selected items. When passed in the selected items will be filtered from the available items.
    model.filterBooleanSet to false to hide the filter
    - - - - - - - - - - - - - -
    ReturnsTypeDetails
    model.selectedItemObjectThe selected item
    - -

    Macro Picker

    -Opens a media picker.
    -view: macropicker - - - - - - - - - - - - - - - -
    ParamTypeDetails
    model.dialogDataObjectObject which contains array of allowedMacros. Set to null to allow all.
    - - - - - - - - - - - - - - - - - - - - -
    ReturnsTypeDetails
    model.macroParamsArrayArray of macro params
    model.selectedMacroObjectThe selected macro
    - -

    Media Picker

    -Opens a media picker.
    -view: mediapicker - - - - - - - - - - - - - - - - - - - - - - - - - -
    ParamTypeDetails
    model.multiPickerBooleanPick one or multiple items
    model.onlyImagesBooleanOnly display files that have an image file-extension
    model.disableFolderSelectBooleanDisable folder selection
    - - - - - - - - - - - - - - - -
    ReturnsTypeDetails
    model.selectedImagesArrayArray of selected images
    - -

    Member Group Picker

    -Opens a member group picker.
    -view: membergrouppicker - - - - - - - - - - - - - - - -
    ParamTypeDetails
    model.multiPickerBooleanPick one or multiple items
    - - - - - - - - - - - - - - - - - - - - -
    ReturnsTypeDetails
    model.selectedMemberGroupStringThe selected member group
    model.selectedMemberGroups (multiPicker)ArrayThe selected member groups
    - -

    Member Picker

    -Opens a member picker.
    -view: memberpicker - - - - - - - - - - - - - - - -
    ParamTypeDetails
    model.multiPickerBooleanPick one or multiple items
    - - - - - - - - - - - - - - -
    ReturnsTypeDetails
    model.selectionArrayArray of selected members/td> -
    - -

    YSOD

    -Opens an overlay to show a custom YSOD.
    -view: ysod - - - - - - - - - - - - - - - -
    ParamTypeDetails
    model.errorObjectError object
    - -@param {object} model Overlay options. -@param {string} view Path to view or one of the default view names. -@param {string} position The overlay position ("left", "right", "center": "target"). -**/ - -(function() { - 'use strict'; - - function OverlayDirective($timeout, formHelper, overlayHelper, localizationService) { - - function link(scope, el, attr, ctrl) { - - scope.directive = { - enableConfirmButton: false - }; - - var overlayNumber = 0; - var numberOfOverlays = 0; - var isRegistered = false; - - var modelCopy = {}; - - function activate() { - - setView(); - - setButtonText(); - - modelCopy = makeModelCopy(scope.model); - - $timeout(function() { - - if (scope.position === "target") { - setTargetPosition(); - } - - // this has to be done inside a timeout to ensure the destroy - // event on other overlays is run before registering a new one - registerOverlay(); - - setOverlayIndent(); - - }); - - } - - function setView() { - - if (scope.view) { - - if (scope.view.indexOf(".html") === -1) { - var viewAlias = scope.view.toLowerCase(); - scope.view = "views/common/overlays/" + viewAlias + "/" + viewAlias + ".html"; - } - - } - - } - - function setButtonText() { - if (!scope.model.closeButtonLabelKey && !scope.model.closeButtonLabel) { - scope.model.closeButtonLabel = localizationService.localize("general_close"); - } - if (!scope.model.submitButtonLabelKey && !scope.model.submitButtonLabel) { - scope.model.submitButtonLabel = localizationService.localize("general_submit"); - } - } - - function registerOverlay() { - - overlayNumber = overlayHelper.registerOverlay(); - - $(document).bind("keydown.overlay-" + overlayNumber, function(event) { - - if (event.which === 27) { - - numberOfOverlays = overlayHelper.getNumberOfOverlays(); - - if (numberOfOverlays === overlayNumber) { - scope.$apply(function () { - scope.closeOverLay(); - }); - } - - event.preventDefault(); - } - - if (event.which === 13) { - - numberOfOverlays = overlayHelper.getNumberOfOverlays(); - - if(numberOfOverlays === overlayNumber) { - - var activeElementType = document.activeElement.tagName; - var clickableElements = ["A", "BUTTON"]; - var submitOnEnter = document.activeElement.hasAttribute("overlay-submit-on-enter"); - - if(clickableElements.indexOf(activeElementType) === 0) { - document.activeElement.click(); - event.preventDefault(); - } else if(activeElementType === "TEXTAREA" && !submitOnEnter) { - - - } else { - scope.$apply(function () { - scope.submitForm(scope.model); + //This will deleteAnimations to true after the current digest + function enableDeleteAnimations() { + //do timeout so that it re-enables them after this digest + $timeout(function () { + //enable delete animations + deleteAnimations = true; + }, 0, false); + } + /** Returns the css classses assigned to the node (div element) */ + scope.getNodeCssClass = function (node) { + if (!node) { + return ''; + } + //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time + // it would be better if we could cache the processing. The problem is that some of these things are dynamic. + var css = []; + if (node.cssClasses) { + _.each(node.cssClasses, function (c) { + css.push(c); }); - event.preventDefault(); - } - - } - - } - - }); - - isRegistered = true; - - } - - function unregisterOverlay() { - - if(isRegistered) { - - overlayHelper.unregisterOverlay(); - - $(document).unbind("keydown.overlay-" + overlayNumber); - - isRegistered = false; - } - - } - - function makeModelCopy(object) { - - var newObject = {}; - - for (var key in object) { - if (key !== "event") { - newObject[key] = angular.copy(object[key]); - } - } - - return newObject; - - } - - function setOverlayIndent() { - - var overlayIndex = overlayNumber - 1; - var indentSize = overlayIndex * 20; - var overlayWidth = el.context.clientWidth; - - el.css('width', overlayWidth - indentSize); - - if(scope.position === "center" || scope.position === "target") { - var overlayTopPosition = el.context.offsetTop; - el.css('top', overlayTopPosition + indentSize); - } - - } - - function setTargetPosition() { - - var container = $("#contentwrapper"); - var containerLeft = container[0].offsetLeft; - var containerRight = containerLeft + container[0].offsetWidth; - var containerTop = container[0].offsetTop; - var containerBottom = containerTop + container[0].offsetHeight; - - var mousePositionClickX = null; - var mousePositionClickY = null; - var elementHeight = null; - var elementWidth = null; - - var position = { - right: "inherit", - left: "inherit", - top: "inherit", - bottom: "inherit" - }; - - // if mouse click position is know place element with mouse in center - if (scope.model.event && scope.model.event) { - - // click position - mousePositionClickX = scope.model.event.pageX; - mousePositionClickY = scope.model.event.pageY; - - // element size - elementHeight = el.context.clientHeight; - elementWidth = el.context.clientWidth; - - // move element to this position - position.left = mousePositionClickX - (elementWidth / 2); - position.top = mousePositionClickY - (elementHeight / 2); - - // check to see if element is outside screen - // outside right - if (position.left + elementWidth > containerRight) { - position.right = 10; - position.left = "inherit"; - } - - // outside bottom - if (position.top + elementHeight > containerBottom) { - position.bottom = 10; - position.top = "inherit"; - } - - // outside left - if (position.left < containerLeft) { - position.left = containerLeft + 10; - position.right = "inherit"; - } - - // outside top - if (position.top < containerTop) { - position.top = 10; - position.bottom = "inherit"; - } - - el.css(position); - - } - - } - - scope.submitForm = function(model) { - if(scope.model.submit) { - if (formHelper.submitForm({scope: scope})) { - formHelper.resetForm({ scope: scope }); - - if(scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { - scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); + } + if (node.selected) { + css.push('umb-tree-node-checked'); + } + return css.join(' '); + }; + //add a method to the node which we can use to call to update the node data if we need to , + // this is done by sync tree, we don't want to add a $watch for each node as that would be crazy insane slow + // so we have to do this + scope.node.updateNodeData = function (newNode) { + _.extend(scope.node, newNode); + //now update the styles + setupNodeDom(scope.node, scope.tree); + }; + /** + Method called when the options button next to a node is called + In the main tree this opens the menu, but internally the tree doesnt + know about this, so it simply raises an event to tell the parent controller + about it. + */ + scope.options = function (n, ev) { + emitEvent('treeOptionsClick', { + element: element, + tree: scope.tree, + node: n, + event: ev + }); + }; + /** + Method called when an item is clicked in the tree, this passes the + DOM element, the tree node object and the original click + and emits it as a treeNodeSelect element if there is a callback object + defined on the tree + */ + scope.select = function (n, ev) { + if (ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.button && ev.button === 1 // middle click, >IE9 + everyone else +) { + return; + } + if (n.metaData && n.metaData.noAccess === true) { + ev.preventDefault(); + return; + } + emitEvent('treeNodeSelect', { + element: element, + tree: scope.tree, + node: n, + event: ev + }); + ev.preventDefault(); + }; + /** + Method called when an item is right-clicked in the tree, this passes the + DOM element, the tree node object and the original click + and emits it as a treeNodeSelect element if there is a callback object + defined on the tree + */ + scope.altSelect = function (n, ev) { + emitEvent('treeNodeAltSelect', { + element: element, + tree: scope.tree, + node: n, + event: ev + }); + }; + /** method to set the current animation for the node. + * This changes dynamically based on if we are changing sections or just loading normal tree data. + * When changing sections we don't want all of the tree-ndoes to do their 'leave' animations. + */ + scope.animation = function () { + if (scope.node.showHideAnimation) { + return scope.node.showHideAnimation; + } + if (deleteAnimations && scope.node.expanded) { + return { leave: 'tree-node-delete-leave' }; } else { - unregisterOverlay(); - scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); - } - - } - } - }; - - scope.cancelConfirmSubmit = function() { - scope.model.confirmSubmit.show = false; - }; - - scope.closeOverLay = function() { - - unregisterOverlay(); - - if (scope.model.close) { - scope.model = modelCopy; - scope.model.close(scope.model); - } else { - scope.model.show = false; - scope.model = null; - } - - }; - - // angular does not support ng-show on custom directives - // width isolated scopes. So we have to make our own. - if (attr.hasOwnProperty("ngShow")) { - scope.$watch("ngShow", function(value) { - if (value) { - el.show(); - activate(); - } else { - unregisterOverlay(); - el.hide(); - } - }); - } else { - activate(); - } - - scope.$on('$destroy', function(){ - unregisterOverlay(); - }); - - } - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/overlays/umb-overlay.html', - scope: { - ngShow: "=", - model: "=", - view: "=", - position: "@" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbOverlay', OverlayDirective); - -})(); - -(function() { - 'use strict'; - - function OverlayBackdropDirective(overlayHelper) { - - function link(scope, el, attr, ctrl) { - - scope.numberOfOverlays = 0; - - scope.$watch(function(){ - return overlayHelper.getNumberOfOverlays(); - }, function (newValue) { - scope.numberOfOverlays = newValue; - }); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/overlays/umb-overlay-backdrop.html', - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbOverlayBackdrop', OverlayBackdropDirective); - -})(); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbProperty -* @restrict E -**/ -angular.module("umbraco.directives") - .directive('umbProperty', function (umbPropEditorHelper) { - return { - scope: { - property: "=" - }, - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/property/umb-property.html', - link: function(scope) { - scope.propertyAlias = Umbraco.Sys.ServerVariables.isDebuggingEnabled === true ? scope.property.alias : null; - }, - //Define a controller for this directive to expose APIs to other directives - controller: function ($scope, $timeout) { - - var self = this; - - //set the API properties/methods - - self.property = $scope.property; - self.setPropertyError = function(errorMsg) { - $scope.property.propertyErrorMessage = errorMsg; + return {}; + } + }; + /** + Method called when a node in the tree is expanded, when clicking the arrow + takes the arrow DOM element and node data as parameters + emits treeNodeCollapsing event if already expanded and treeNodeExpanding if collapsed + */ + scope.load = function (node) { + if (node.expanded && !node.metaData.isContainer) { + deleteAnimations = false; + emitEvent('treeNodeCollapsing', { + tree: scope.tree, + node: node, + element: element + }); + node.expanded = false; + } else { + scope.loadChildren(node, false); + } + }; + /* helper to force reloading children of a tree node */ + scope.loadChildren = function (node, forceReload) { + //emit treeNodeExpanding event, if a callback object is set on the tree + emitEvent('treeNodeExpanding', { + tree: scope.tree, + node: node + }); + if (node.hasChildren && (forceReload || !node.children || angular.isArray(node.children) && node.children.length === 0)) { + //get the children from the tree service + treeService.loadNodeChildren({ + node: node, + section: scope.section + }).then(function (data) { + //emit expanded event + emitEvent('treeNodeExpanded', { + tree: scope.tree, + node: node, + children: data + }); + enableDeleteAnimations(); + }); + } else { + emitEvent('treeNodeExpanded', { + tree: scope.tree, + node: node, + children: node.children + }); + node.expanded = true; + enableDeleteAnimations(); + } }; + //if the current path contains the node id, we will auto-expand the tree item children + setupNodeDom(scope.node, scope.tree); + // load the children if the current user don't have access to the node + // it is used to auto expand the tree to the start nodes the user has access to + if (scope.node.hasChildren && scope.node.metaData.noAccess) { + scope.loadChildren(scope.node); + } + var template = '
    '; + var newElement = angular.element(template); + $compile(newElement)(scope); + element.append(newElement); } }; - }); -/** -* @ngdoc directive -* @function -* @name umbraco.directives.directive:umbPropertyEditor -* @requires formController -* @restrict E + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbTreeSearchBox +* @function +* @element ANY +* @restrict E **/ - -//share property editor directive function -var _umbPropertyEditor = function (umbPropEditorHelper) { + function treeSearchBox(localizationService, searchService, $q) { return { scope: { - model: "=", - isPreValue: "@", - preview: "@" + searchFromId: '@', + searchFromName: '@', + showSearch: '@', + section: '@', + hideSearchCallback: '=', + searchCallback: '=' }, - - require: "^form", restrict: 'E', - replace: true, - templateUrl: 'views/components/property/umb-property-editor.html', + // restrict to an element + replace: true, + // replace the html element with the template + templateUrl: 'views/components/tree/umb-tree-search-box.html', link: function (scope, element, attrs, ctrl) { - - //we need to copy the form controller val to our isolated scope so that - //it get's carried down to the child scopes of this! - //we'll also maintain the current form name. - scope[ctrl.$name] = ctrl; - - if(!scope.model.alias){ - scope.model.alias = Math.random().toString(36).slice(2); - } - - scope.$watch("model.view", function(val){ - scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue); + scope.term = ''; + scope.hideSearch = function () { + scope.term = ''; + scope.hideSearchCallback(); + }; + localizationService.localize('general_typeToSearch').then(function (value) { + scope.searchPlaceholderText = value; }); + if (!scope.showSearch) { + scope.showSearch = 'false'; + } + //used to cancel any request in progress if another one needs to take it's place + var canceler = null; + function performSearch() { + if (scope.term) { + scope.results = []; + //a canceler exists, so perform the cancelation operation and reset + if (canceler) { + canceler.resolve(); + canceler = $q.defer(); + } else { + canceler = $q.defer(); + } + var searchArgs = { + term: scope.term, + canceler: canceler + }; + //append a start node context if there is one + if (scope.searchFromId) { + searchArgs['searchFrom'] = scope.searchFromId; + } + searcher(searchArgs).then(function (data) { + scope.searchCallback(data); + //set back to null so it can be re-created + canceler = null; + }); + } + } + scope.$watch('term', _.debounce(function (newVal, oldVal) { + scope.$apply(function () { + if (newVal !== null && newVal !== undefined && newVal !== oldVal) { + performSearch(); + } + }); + }, 200)); + var searcher = searchService.searchContent; + //search + if (scope.section === 'member') { + searcher = searchService.searchMembers; + } else if (scope.section === 'media') { + searcher = searchService.searchMedia; + } } }; - }; - -//Preffered is the umb-property-editor as its more explicit - but we keep umb-editor for backwards compat -angular.module("umbraco.directives").directive('umbPropertyEditor', _umbPropertyEditor); -angular.module("umbraco.directives").directive('umbEditor', _umbPropertyEditor); - -angular.module("umbraco.directives.html") - .directive('umbPropertyGroup', function () { + } + angular.module('umbraco.directives').directive('umbTreeSearchBox', treeSearchBox); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbTreeSearchResults +* @function +* @element ANY +* @restrict E +**/ + function treeSearchResults() { return { - transclude: true, + scope: { + results: '=', + selectResultCallback: '=' + }, restrict: 'E', - replace: true, - templateUrl: 'views/components/property/umb-property-group.html' - }; - }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTab -* @restrict E -**/ -angular.module("umbraco.directives") -.directive('umbTab', function ($parse, $timeout) { - return { - restrict: 'E', - replace: true, - transclude: 'true', - templateUrl: 'views/components/tabs/umb-tab.html' - }; -}); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTabs -* @restrict A -* @description Used to bind to bootstrap tab events so that sub directives can use this API to listen to tab changes -**/ -angular.module("umbraco.directives") -.directive('umbTabs', function () { - return { - restrict: 'A', - controller: function ($scope, $element, $attrs) { - - var callbacks = []; - this.onTabShown = function(cb) { - callbacks.push(cb); - }; - - function tabShown(event) { - - var curr = $(event.target); // active tab - var prev = $(event.relatedTarget); // previous tab - - $scope.$apply(); - - for (var c in callbacks) { - callbacks[c].apply(this, [{current: curr, previous: prev}]); - } + // restrict to an element + replace: true, + // replace the html element with the template + templateUrl: 'views/components/tree/umb-tree-search-results.html', + link: function (scope, element, attrs, ctrl) { } - - //NOTE: it MUST be done this way - binding to an ancestor element that exists - // in the DOM to bind to the dynamic elements that will be created. - // It would be nicer to create this event handler as a directive for which child - // directives can attach to. - $element.on('shown', '.nav-tabs a', tabShown); - - //ensure to unregister - $scope.$on('$destroy', function () { - $element.off('shown', '.nav-tabs a', tabShown); - - for (var c in callbacks) { - delete callbacks[c]; - } - callbacks = null; - }); - } - }; -}); -(function() { - 'use strict'; - - function UmbTabsContentDirective() { - - function link(scope, el, attr, ctrl) { - - scope.view = attr.view; - - } - - var directive = { - restrict: "E", - replace: true, - transclude: 'true', - templateUrl: "views/components/tabs/umb-tabs-content.html", - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbTabsContent', UmbTabsContentDirective); - -})(); - -(function() { - 'use strict'; - - function UmbTabsNavDirective($timeout) { - - function link(scope, el, attr) { - - function activate() { - - $timeout(function () { - - //use bootstrap tabs API to show the first one - el.find("a:first").tab('show'); - - //enable the tab drop - el.tabdrop(); - - }); - - } - - var unbindModelWatch = scope.$watch('model', function(newValue, oldValue){ - - activate(); - - }); - - - scope.$on('$destroy', function () { - - //ensure to destroy tabdrop (unbinds window resize listeners) - el.tabdrop("destroy"); - - unbindModelWatch(); - - }); - + }; } - - var directive = { - restrict: "E", - replace: true, - templateUrl: "views/components/tabs/umb-tabs-nav.html", - scope: { - model: "=", - tabdrop: "=", - idSuffix: "@" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbTabsNav', UmbTabsNavDirective); - -})(); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTree -* @restrict E -**/ -function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificationsService, $timeout, userService) { - - return { - restrict: 'E', - replace: true, - terminal: false, - - scope: { - section: '@', - treealias: '@', - hideoptions: '@', - hideheader: '@', - cachekey: '@', - isdialog: '@', - onlyinitialized: '@', - //Custom query string arguments to pass in to the tree as a string, example: "startnodeid=123&something=value" - customtreeparams: '@', - eventhandler: '=', - enablecheckboxes: '@', - enablelistviewsearch: '@', - enablelistviewexpand: '@' - }, - - compile: function(element, attrs) { - //config - //var showheader = (attrs.showheader !== 'false'); - var hideoptions = (attrs.hideoptions === 'true') ? "hide-options" : ""; - var template = '
    • '; - template += '
      ' + - '
      ' + - ' {{tree.name}}
      ' + - '' + - '
      '; - template += '
        ' + - '' + - '
      ' + - '
    • ' + - '
    '; - - element.replaceWith(template); - - return function(scope, elem, attr, controller) { - - //flag to track the last loaded section when the tree 'un-loads'. We use this to determine if we should - // re-load the tree again. For example, if we hover over 'content' the content tree is shown. Then we hover - // outside of the tree and the tree 'un-loads'. When we re-hover over 'content', we don't want to re-load the - // entire tree again since we already still have it in memory. Of course if the section is different we will - // reload it. This saves a lot on processing if someone is navigating in and out of the same section many times - // since it saves on data retreival and DOM processing. - var lastSection = ""; - - //setup a default internal handler - if (!scope.eventhandler) { - scope.eventhandler = $({}); - } - - //flag to enable/disable delete animations - var deleteAnimations = false; - - - /** Helper function to emit tree events */ - function emitEvent(eventName, args) { - if (scope.eventhandler) { - $(scope.eventhandler).trigger(eventName, args); - } - } - - /** This will deleteAnimations to true after the current digest */ - function enableDeleteAnimations() { - //do timeout so that it re-enables them after this digest - $timeout(function () { - //enable delete animations - deleteAnimations = true; - }, 0, false); - } - - - /*this is the only external interface a tree has */ - function setupExternalEvents() { - if (scope.eventhandler) { - - scope.eventhandler.clearCache = function(section) { - treeService.clearCache({ section: section }); - }; - - scope.eventhandler.load = function(section) { - scope.section = section; - loadTree(); - }; - - scope.eventhandler.reloadNode = function(node) { - - if (!node) { - node = scope.currentNode; - } - - if (node) { - scope.loadChildren(node, true); - } - }; - + angular.module('umbraco.directives').directive('umbTreeSearchResults', treeSearchResults); + (function () { + 'use strict'; + function AceEditorDirective(umbAceEditorConfig, assetsService, angularHelper) { + /** + * Sets editor options such as the wrapping mode or the syntax checker. + * + * The supported options are: + * + *
      + *
    • showGutter
    • + *
    • useWrapMode
    • + *
    • onLoad
    • + *
    • theme
    • + *
    • mode
    • + *
    + * + * @param acee + * @param session ACE editor session + * @param {object} opts Options to be set + */ + var setOptions = function (acee, session, opts) { + // sets the ace worker path, if running from concatenated + // or minified source + if (angular.isDefined(opts.workerPath)) { + var config = window.ace.require('ace/config'); + config.set('workerPath', opts.workerPath); + } + // ace requires loading + if (angular.isDefined(opts.require)) { + opts.require.forEach(function (n) { + window.ace.require(n); + }); + } + // Boolean options + if (angular.isDefined(opts.showGutter)) { + acee.renderer.setShowGutter(opts.showGutter); + } + if (angular.isDefined(opts.useWrapMode)) { + session.setUseWrapMode(opts.useWrapMode); + } + if (angular.isDefined(opts.showInvisibles)) { + acee.renderer.setShowInvisibles(opts.showInvisibles); + } + if (angular.isDefined(opts.showIndentGuides)) { + acee.renderer.setDisplayIndentGuides(opts.showIndentGuides); + } + if (angular.isDefined(opts.useSoftTabs)) { + session.setUseSoftTabs(opts.useSoftTabs); + } + if (angular.isDefined(opts.showPrintMargin)) { + acee.setShowPrintMargin(opts.showPrintMargin); + } + // commands + if (angular.isDefined(opts.disableSearch) && opts.disableSearch) { + acee.commands.addCommands([{ + name: 'unfind', + bindKey: { + win: 'Ctrl-F', + mac: 'Command-F' + }, + exec: function () { + return false; + }, + readOnly: true + }]); + } + // Basic options + if (angular.isString(opts.theme)) { + acee.setTheme('ace/theme/' + opts.theme); + } + if (angular.isString(opts.mode)) { + session.setMode('ace/mode/' + opts.mode); + } + // Advanced options + if (angular.isDefined(opts.firstLineNumber)) { + if (angular.isNumber(opts.firstLineNumber)) { + session.setOption('firstLineNumber', opts.firstLineNumber); + } else if (angular.isFunction(opts.firstLineNumber)) { + session.setOption('firstLineNumber', opts.firstLineNumber()); + } + } + // advanced options + var key, obj; + if (angular.isDefined(opts.advanced)) { + for (key in opts.advanced) { + // create a javascript object with the key and value + obj = { + name: key, + value: opts.advanced[key] + }; + // try to assign the option to the ace editor + acee.setOption(obj.name, obj.value); + } + } + // advanced options for the renderer + if (angular.isDefined(opts.rendererOptions)) { + for (key in opts.rendererOptions) { + // create a javascript object with the key and value + obj = { + name: key, + value: opts.rendererOptions[key] + }; + // try to assign the option to the ace editor + acee.renderer.setOption(obj.name, obj.value); + } + } + // onLoad callbacks + angular.forEach(opts.callbacks, function (cb) { + if (angular.isFunction(cb)) { + cb(acee); + } + }); + }; + function link(scope, el, attr, ngModel) { + // Load in ace library + assetsService.load([ + 'lib/ace-builds/src-min-noconflict/ace.js', + 'lib/ace-builds/src-min-noconflict/ext-language_tools.js' + ]).then(function () { + if (angular.isUndefined(window.ace)) { + throw new Error('ui-ace need ace to work... (o rly?)'); + } else { + // init editor + init(); + } + }); + function init() { + /** + * Corresponds the umbAceEditorConfig ACE configuration. + * @type object + */ + var options = umbAceEditorConfig.ace || {}; + /** + * umbAceEditorConfig merged with user options via json in attribute or data binding + * @type object + */ + var opts = angular.extend({}, options, scope.umbAceEditor); + //load ace libraries here... + /** + * ACE editor + * @type object + */ + var acee = window.ace.edit(el[0]); + acee.$blockScrolling = Infinity; + /** + * ACE editor session. + * @type object + * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session} + */ + var session = acee.getSession(); + /** + * Reference to a change listener created by the listener factory. + * @function + * @see listenerFactory.onChange + */ + var onChangeListener; + /** + * Reference to a blur listener created by the listener factory. + * @function + * @see listenerFactory.onBlur + */ + var onBlurListener; + /** + * Calls a callback by checking its existing. The argument list + * is variable and thus this function is relying on the arguments + * object. + * @throws {Error} If the callback isn't a function + */ + var executeUserCallback = function () { /** - Used to do the tree syncing. If the args.tree is not specified we are assuming it has been - specified previously using the _setActiveTreeType - */ - scope.eventhandler.syncTree = function(args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.path) { - throw "args.path cannot be null"; - } - - var deferred = $q.defer(); - - //this is super complex but seems to be working in other places, here we're listening for our - // own events, once the tree is sycned we'll resolve our promise. - scope.eventhandler.one("treeSynced", function (e, syncArgs) { - deferred.resolve(syncArgs); - }); - - //this should normally be set unless it is being called from legacy - // code, so set the active tree type before proceeding. - if (args.tree) { - loadActiveTree(args.tree); - } - - if (angular.isString(args.path)) { - args.path = args.path.replace('"', '').split(','); - } - - //reset current node selection - //scope.currentNode = null; - - //Filter the path for root node ids (we don't want to pass in -1 or 'init') - - args.path = _.filter(args.path, function (item) { return (item !== "init" && item !== "-1"); }); - - //Once those are filtered we need to check if the current user has a special start node id, - // if they do, then we're going to trim the start of the array for anything found from that start node - // and previous so that the tree syncs properly. The tree syncs from the top down and if there are parts - // of the tree's path in there that don't actually exist in the dom/model then syncing will not work. + * The callback function grabbed from the array-like arguments + * object. The first argument should always be the callback. + * + * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} + * @type {*} + */ + var callback = arguments[0]; + /** + * Arguments to be passed to the callback. These are taken + * from the array-like arguments object. The first argument + * is stripped because that should be the callback function. + * + * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} + * @type {Array} + */ + var args = Array.prototype.slice.call(arguments, 1); + if (angular.isDefined(callback)) { + scope.$evalAsync(function () { + if (angular.isFunction(callback)) { + callback(args); + } else { + throw new Error('ui-ace use a function as callback.'); + } + }); + } + }; + /** + * Listener factory. Until now only change listeners can be created. + * @type object + */ + var listenerFactory = { + /** + * Creates a change listener which propagates the change event + * and the editor session to the callback from the user option + * onChange. It might be exchanged during runtime, if this + * happens the old listener will be unbound. + * + * @param callback callback function defined in the user options + * @see onChangeListener + */ + onChange: function (callback) { + return function (e) { + var newValue = session.getValue(); + angularHelper.safeApply(scope, function () { + scope.model = newValue; + }); + executeUserCallback(callback, e, acee); + }; + }, + /** + * Creates a blur listener which propagates the editor session + * to the callback from the user option onBlur. It might be + * exchanged during runtime, if this happens the old listener + * will be unbound. + * + * @param callback callback function defined in the user options + * @see onBlurListener + */ + onBlur: function (callback) { + return function () { + executeUserCallback(callback, acee); + }; + } + }; + attr.$observe('readonly', function (value) { + acee.setReadOnly(!!value || value === ''); + }); + // Value Blind + if (scope.model) { + session.setValue(scope.model); + } + // Listen for option updates + var updateOptions = function (current, previous) { + if (current === previous) { + return; + } + opts = angular.extend({}, options, scope.umbAceEditor); + opts.callbacks = [opts.onLoad]; + if (opts.onLoad !== options.onLoad) { + // also call the global onLoad handler + opts.callbacks.unshift(options.onLoad); + } + // EVENTS + // unbind old change listener + session.removeListener('change', onChangeListener); + // bind new change listener + onChangeListener = listenerFactory.onChange(opts.onChange); + session.on('change', onChangeListener); + // unbind old blur listener + //session.removeListener('blur', onBlurListener); + acee.removeListener('blur', onBlurListener); + // bind new blur listener + onBlurListener = listenerFactory.onBlur(opts.onBlur); + acee.on('blur', onBlurListener); + setOptions(acee, session, opts); + }; + scope.$watch(scope.umbAceEditor, updateOptions, /* deep watch */ + true); + // set the options here, even if we try to watch later, if this + // line is missing things go wrong (and the tests will also fail) + updateOptions(options); + el.on('$destroy', function () { + acee.session.$stopWorker(); + acee.destroy(); + }); + scope.$watch(function () { + return [ + el[0].offsetWidth, + el[0].offsetHeight + ]; + }, function () { + acee.resize(); + acee.renderer.updateFull(); + }, true); + } + } + var directive = { + restrict: 'EA', + scope: { + 'umbAceEditor': '=', + 'model': '=' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').constant('umbAceEditorConfig', {}).directive('umbAceEditor', AceEditorDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbAvatar +@restrict E +@scope - userService.getCurrentUser().then(function(userData) { +@description +Use this directive to render an avatar. - var startNodes = [userData.startContentId, userData.startMediaId]; - _.each(startNodes, function (i) { - var found = _.find(args.path, function (p) { - return String(p) === String(i); - }); - if (found) { - args.path = args.path.splice(_.indexOf(args.path, found)); - } - }); +

    Markup example

    +
    +	
    + + - loadPath(args.path, args.forceReload, args.activate); +
    +
    - }); +

    Controller example

    +
    +	(function () {
    +		"use strict";
     
    +		function Controller() {
     
    +            var vm = this;
     
    -                            return deferred.promise;
    -                        };
    +            vm.avatar = [
    +                { value: "assets/logo.png" },
    +                { value: "assets/logo@2x.png" },
    +                { value: "assets/logo@3x.png" }
    +            ];
     
    -                        /**
    -                            Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to
    -                            have to set an active tree and then sync, the new API does this in one method by using syncTree.
    -                            loadChildren is optional but if it is set, it will set the current active tree and load the root
    -                            node's children - this is synonymous with the legacy refreshTree method - again should not be used
    -                            and should only be used for the legacy code to work.
    -                        */
    -                        scope.eventhandler._setActiveTreeType = function(treeAlias, loadChildren) {
    -                            loadActiveTree(treeAlias, loadChildren);
    -                        };
    -                    }
    -                }
    +        }
     
    +		angular.module("umbraco").controller("My.Controller", Controller);
     
    -                //helper to load a specific path on the active tree as soon as its ready
    -                function loadPath(path, forceReload, activate) {
    +	})();
    +
    - if (scope.activeTree) { - syncTree(scope.activeTree, path, forceReload, activate); - } - else { - scope.eventhandler.one("activeTreeLoaded", function (e, args) { - syncTree(args.tree, path, forceReload, activate); - }); - } +@param {string} size (attribute): The size of the avatar (xs, s, m, l, xl). +@param {string} img-src (attribute): The image source to the avatar. +@param {string} img-srcset (atribute): Reponsive support for the image source. +**/ + (function () { + 'use strict'; + function AvatarDirective() { + function link(scope, element, attrs, ctrl) { + var eventBindings = []; + scope.initials = ''; + function onInit() { + if (!scope.unknownChar) { + scope.unknownChar = '?'; + } + scope.initials = getNameInitials(scope.name); + } + function getNameInitials(name) { + if (name) { + var names = name.split(' '), initials = names[0].substring(0, 1); + if (names.length > 1) { + initials += names[names.length - 1].substring(0, 1); + } + return initials.toUpperCase(); + } + return null; + } + eventBindings.push(scope.$watch('name', function (newValue, oldValue) { + if (newValue === oldValue) { + return; + } + if (oldValue === undefined || newValue === undefined) { + return; + } + scope.initials = getNameInitials(newValue); + })); + onInit(); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-avatar.html', + scope: { + size: '@', + name: '@', + color: '@', + imgSrc: '@', + imgSrcset: '@', + unknownChar: '@' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbAvatar', AvatarDirective); + }()); + (function () { + 'use strict'; + function BadgeDirective() { + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/umb-badge.html', + scope: { + size: '@?', + color: '@?' + } + }; + return directive; + } + angular.module('umbraco.directives').directive('umbBadge', BadgeDirective); + }()); + (function () { + 'use strict'; + function CheckmarkDirective() { + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/umb-checkmark.html', + scope: { + size: '@?', + checked: '=' + } + }; + return directive; + } + angular.module('umbraco.directives').directive('umbCheckmark', CheckmarkDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbChildSelector +@restrict E +@scope + +@description +Use this directive to render a ui component for selecting child items to a parent node. + +

    Markup example

    +
    +	
    + + + + + + + + +
    +
    + +

    Controller example

    +
    +	(function () {
    +		"use strict";
    +
    +		function Controller() {
    +
    +            var vm = this;
    +
    +            vm.id = 1;
    +            vm.name = "My Parent element";
    +            vm.icon = "icon-document";
    +            vm.selectedChildren = [];
    +            vm.availableChildren = [
    +                {
    +                    id: 1,
    +                    alias: "item1",
    +                    name: "Item 1",
    +                    icon: "icon-document"
    +                },
    +                {
    +                    id: 2,
    +                    alias: "item2",
    +                    name: "Item 2",
    +                    icon: "icon-document"
                     }
    -
    -
    -                //given a tree alias, this will search the current section tree for the specified tree alias and
    -                //set that to the activeTree
    -                //NOTE: loadChildren is ONLY used for legacy purposes, do not use this when syncing the tree as it will cause problems
    -                // since there will be double request and event handling operations.
    -                function loadActiveTree(treeAlias, loadChildren) {
    -                    if (!treeAlias) {
    -                        return;
    -                    }
    -
    -                    scope.activeTree = undefined;
    -
    -                    function doLoad(tree) {
    -                        var childrenAndSelf = [tree].concat(tree.children);
    -                        scope.activeTree = _.find(childrenAndSelf, function (node) {
    -                            if(node && node.metaData && node.metaData.treeAlias) {
    -                                return node.metaData.treeAlias.toUpperCase() === treeAlias.toUpperCase();
    -                            }
    -                            return false;
    -                        });
    -
    -                        if (!scope.activeTree) {
    -                            throw "Could not find the tree " + treeAlias + ", activeTree has not been set";
    -                        }
    -
    -                        //This is only used for the legacy tree method refreshTree!
    -                        if (loadChildren) {
    -                            scope.activeTree.expanded = true;
    -                            scope.loadChildren(scope.activeTree, false).then(function() {
    -                                emitEvent("activeTreeLoaded", { tree: scope.activeTree });
    -                            });
    -                        }
    -                        else {
    -                            emitEvent("activeTreeLoaded", { tree: scope.activeTree });
    -                        }
    -                    }
    -
    -                    if (scope.tree) {
    -                        doLoad(scope.tree.root);
    -                    }
    -                    else {
    -                        scope.eventhandler.one("treeLoaded", function(e, args) {
    -                            doLoad(args.tree.root);
    -                        });
    +            ];
    +
    +            vm.addChild = addChild;
    +            vm.removeChild = removeChild;
    +
    +            function addChild($event) {
    +                vm.overlay = {
    +                    view: "itempicker",
    +                    title: "Choose child",
    +                    availableItems: vm.availableChildren,
    +                    selectedItems: vm.selectedChildren,
    +                    event: $event,
    +                    show: true,
    +                    submit: function(model) {
    +
    +                        // add selected child
    +                        vm.selectedChildren.push(model.selectedItem);
    +
    +                        // close overlay
    +                        vm.overlay.show = false;
    +                        vm.overlay = null;
                         }
    -                }
    -
    -
    -                /** Method to load in the tree data */
    -
    -                function loadTree() {
    -                    if (!scope.loading && scope.section) {
    -                        scope.loading = true;
    -
    -                        //anytime we want to load the tree we need to disable the delete animations
    -                        deleteAnimations = false;
    -
    -                        //default args
    -                        var args = { section: scope.section, tree: scope.treealias, cacheKey: scope.cachekey, isDialog: scope.isdialog ? scope.isdialog : false, onlyinitialized: scope.onlyinitialized };
    +                };
    +            }
     
    -                        //add the extra query string params if specified
    -                        if (scope.customtreeparams) {
    -                            args["queryString"] = scope.customtreeparams;
    -                        }
    +            function removeChild($index) {
    +                vm.selectedChildren.splice($index, 1);
    +            }
     
    -                        treeService.getTree(args)
    -                            .then(function(data) {
    -                                //set the data once we have it
    -                                scope.tree = data;
    +        }
     
    -                                enableDeleteAnimations();
    +		angular.module("umbraco").controller("My.Controller", Controller);
    +
    +	})();
    +
    + +@param {array} selectedChildren (binding): Array of selected children. +@param {array} availableChildren (binding: Array of items available for selection. +@param {string} parentName (binding): The parent name. +@param {string} parentIcon (binding): The parent icon. +@param {number} parentId (binding): The parent id. +@param {callback} onRemove (binding): Callback when the remove button is clicked on an item. +

    The callback returns:

    +
      +
    • child: The selected item.
    • +
    • $index: The selected item index.
    • +
    +@param {callback} onAdd (binding): Callback when the add button is clicked. +

    The callback returns:

    +
      +
    • $event: The select event.
    • +
    +**/ + (function () { + 'use strict'; + function ChildSelectorDirective() { + function link(scope, el, attr, ctrl) { + var eventBindings = []; + scope.dialogModel = {}; + scope.showDialog = false; + scope.removeChild = function (selectedChild, $index) { + if (scope.onRemove) { + scope.onRemove(selectedChild, $index); + } + }; + scope.addChild = function ($event) { + if (scope.onAdd) { + scope.onAdd($event); + } + }; + function syncParentName() { + // update name on available item + angular.forEach(scope.availableChildren, function (availableChild) { + if (availableChild.id === scope.parentId) { + availableChild.name = scope.parentName; + } + }); + // update name on selected child + angular.forEach(scope.selectedChildren, function (selectedChild) { + if (selectedChild.id === scope.parentId) { + selectedChild.name = scope.parentName; + } + }); + } + function syncParentIcon() { + // update icon on available item + angular.forEach(scope.availableChildren, function (availableChild) { + if (availableChild.id === scope.parentId) { + availableChild.icon = scope.parentIcon; + } + }); + // update icon on selected child + angular.forEach(scope.selectedChildren, function (selectedChild) { + if (selectedChild.id === scope.parentId) { + selectedChild.icon = scope.parentIcon; + } + }); + } + eventBindings.push(scope.$watch('parentName', function (newValue, oldValue) { + if (newValue === oldValue) { + return; + } + if (oldValue === undefined || newValue === undefined) { + return; + } + syncParentName(); + })); + eventBindings.push(scope.$watch('parentIcon', function (newValue, oldValue) { + if (newValue === oldValue) { + return; + } + if (oldValue === undefined || newValue === undefined) { + return; + } + syncParentIcon(); + })); + // clean up + scope.$on('$destroy', function () { + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } + }); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-child-selector.html', + scope: { + selectedChildren: '=', + availableChildren: '=', + parentName: '=', + parentIcon: '=', + parentId: '=', + onRemove: '=', + onAdd: '=' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbChildSelector', ChildSelectorDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbClipboard +@restrict E +@scope + +@description +Added in Umbraco v. 7.7: Use this directive to copy content to the clipboard + +

    Markup example

    +
    +    
    + + +
    Copy me!
    + + + + + + + + + + + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +
    +            vm.copyText = "Copy text without element";
    +            vm.cutText = "Text to cut";
    +
    +            vm.copySuccess = copySuccess;
    +            vm.copyError = copyError;
    +
    +            function copySuccess() {
    +                vm.clipboardButtonState = "success";
    +            }
    +            
    +            function copyError() {
    +                vm.clipboardButtonState = "error";
    +            }
     
    -                                scope.loading = false;
    +        }
     
    -                                //set the root as the current active tree
    -                                scope.activeTree = scope.tree.root;
    -                                emitEvent("treeLoaded", { tree: scope.tree });
    -                                emitEvent("treeNodeExpanded", { tree: scope.tree, node: scope.tree.root, children: scope.tree.root.children });
    +        angular.module("umbraco").controller("My.ClipBoardController", Controller);
     
    -                            }, function(reason) {
    -                                scope.loading = false;
    -                                notificationsService.error("Tree Error", reason);
    -                            });
    -                    }
    -                }
    +    })();
    +
    - /** syncs the tree, the treeNode can be ANY tree node in the tree that requires syncing */ - function syncTree(treeNode, path, forceReload, activate) { +@param {callback} umbClipboardSuccess (expression): Callback function when the content is copied. +@param {callback} umbClipboardError (expression): Callback function if the copy fails. +@param {string} umbClipboardTarget (attribute): The target element to copy. +@param {string} umbClipboardAction (attribute): Specify if you want to copy or cut content ("copy", "cut"). Cut only works on input and textarea elements. +@param {string} umbClipboardText (attribute): Use this attribute if you don't have an element to copy from. - deleteAnimations = false; +**/ + (function () { + 'use strict'; + function umbClipboardDirective($timeout, assetsService) { + function link(scope, element, attrs, ctrl) { + var clipboard; + var target = element[0]; + assetsService.loadJs('lib/clipboard/clipboard.min.js').then(function () { + if (scope.umbClipboardTarget) { + target.setAttribute('data-clipboard-target', scope.umbClipboardTarget); + } + if (scope.umbClipboardAction) { + target.setAttribute('data-clipboard-action', scope.umbClipboardAction); + } + if (scope.umbClipboardText) { + target.setAttribute('data-clipboard-text', scope.umbClipboardText); + } + clipboard = new Clipboard(target); + clipboard.on('success', function (e) { + e.clearSelection(); + if (scope.umbClipboardSuccess) { + scope.$apply(function () { + scope.umbClipboardSuccess({ e: e }); + }); + } + }); + clipboard.on('error', function (e) { + if (scope.umbClipboardError) { + scope.$apply(function () { + scope.umbClipboardError({ e: e }); + }); + } + }); + }); + // clean up + scope.$on('$destroy', function () { + clipboard.destroy(); + }); + } + //////////// + var directive = { + restrict: 'A', + scope: { + umbClipboardSuccess: '&?', + umbClipboardError: '&?', + umbClipboardTarget: '@?', + umbClipboardAction: '@?', + umbClipboardText: '=?' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbClipboard', umbClipboardDirective); + }()); + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbConfirm + * @function + * @description + * A confirmation dialog + * + * @restrict E + */ + function confirmDirective() { + return { + restrict: 'E', + // restrict to an element + replace: true, + // replace the html element with the template + templateUrl: 'views/components/umb-confirm.html', + scope: { + onConfirm: '=', + onCancel: '=', + caption: '@' + }, + link: function (scope, element, attr, ctrl) { + } + }; + } + angular.module('umbraco.directives').directive('umbConfirm', confirmDirective); + /** +@ngdoc directive +@name umbraco.directives.directive:umbConfirmAction +@restrict E +@scope + +@description +

    Use this directive to toggle a confirmation prompt for an action. +The prompt consists of a checkmark and a cross to confirm or cancel the action. +The prompt can be opened in four direction up, down, left or right.

    + +

    Markup example

    +
    +    
    + +
    + + + +
    + +
    +
    + +

    Controller example

    +
    +    (function () {
    +
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +            vm.promptIsVisible = false;
    +
    +            vm.confirmAction = confirmAction;
    +            vm.showPrompt = showPrompt;
    +            vm.hidePrompt = hidePrompt;
    +
    +            function confirmAction() {
    +                // confirm logic here
    +            }
     
    -                    treeService.syncTree({
    -                        node: treeNode,
    -                        path: path,
    -                        forceReload: forceReload
    -                    }).then(function (data) {
    +            function showPrompt() {
    +                vm.promptIsVisible = true;
    +            }
     
    -                        if (activate === undefined || activate === true) {
    -                            scope.currentNode = data;
    -                        }
    +            function hidePrompt() {
    +                vm.promptIsVisible = false;
    +            }
     
    -                        emitEvent("treeSynced", { node: data, activate: activate });
    +        }
     
    -                        enableDeleteAnimations();
    -                    });
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +    })();
    +
    +@param {string} direction The direction the prompt opens ("up", "down", "left", "right"). +@param {callback} onConfirm Callback when the checkmark is clicked. +@param {callback} onCancel Callback when the cross is clicked. +**/ + (function () { + 'use strict'; + function ConfirmAction() { + function link(scope, el, attr, ctrl) { + scope.clickConfirm = function () { + if (scope.onConfirm) { + scope.onConfirm(); + } + }; + scope.clickCancel = function () { + if (scope.onCancel) { + scope.onCancel(); + } + }; + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-confirm-action.html', + scope: { + direction: '@', + onConfirm: '&', + onCancel: '&' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbConfirmAction', ConfirmAction); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbContentGrid +@restrict E +@scope + +@description +Use this directive to generate a list of content items presented as a flexbox grid. + +

    Markup example

    +
    +    
    + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +            vm.contentItems = [
    +                {
    +                    "name": "Cape",
    +                    "published": true,
    +                    "icon": "icon-document",
    +                    "updateDate": "15-02-2016",
    +                    "owner": "Mr. Batman",
    +                    "selected": false
    +                },
    +                {
    +                    "name": "Utility Belt",
    +                    "published": true,
    +                    "icon": "icon-document",
    +                    "updateDate": "15-02-2016",
    +                    "owner": "Mr. Batman",
    +                    "selected": false
    +                },
    +                {
    +                    "name": "Cave",
    +                    "published": true,
    +                    "icon": "icon-document",
    +                    "updateDate": "15-02-2016",
    +                    "owner": "Mr. Batman",
    +                    "selected": false
                     }
    +            ];
    +            vm.includeProperties = [
    +                {
    +                  "alias": "updateDate",
    +                  "header": "Last edited"
    +                },
    +                {
    +                  "alias": "owner",
    +                  "header": "Created by"
    +                }
    +            ];
     
    -                scope.selectEnabledNodeClass = function (node) {
    -                    return node ?
    -                        node.selected ?
    -                        'icon umb-tree-icon sprTree icon-check green temporary' :
    -                        '' :
    -                        '';
    -                };
    -
    -                /** method to set the current animation for the node.
    -                 *  This changes dynamically based on if we are changing sections or just loading normal tree data.
    -                 *  When changing sections we don't want all of the tree-ndoes to do their 'leave' animations.
    -                 */
    -                scope.animation = function() {
    -                    if (deleteAnimations && scope.tree && scope.tree.root && scope.tree.root.expanded) {
    -                        return { leave: 'tree-node-delete-leave' };
    -                    }
    -                    else {
    -                        return {};
    -                    }
    -                };
    -
    -                /* helper to force reloading children of a tree node */
    -                scope.loadChildren = function(node, forceReload) {
    -                    var deferred = $q.defer();
    -
    -                    //emit treeNodeExpanding event, if a callback object is set on the tree
    -                    emitEvent("treeNodeExpanding", { tree: scope.tree, node: node });
    -
    -                    //standardising
    -                    if (!node.children) {
    -                        node.children = [];
    -                    }
    -
    -                    if (forceReload || (node.hasChildren && node.children.length === 0)) {
    -                        //get the children from the tree service
    -                        treeService.loadNodeChildren({ node: node, section: scope.section })
    -                            .then(function(data) {
    -                                //emit expanded event
    -                                emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: data });
    -
    -                                enableDeleteAnimations();
    -
    -                                deferred.resolve(data);
    -                            });
    -                    }
    -                    else {
    -                        emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: node.children });
    -                        node.expanded = true;
    -
    -                        enableDeleteAnimations();
    -
    -                        deferred.resolve(node.children);
    -                    }
    -
    -                    return deferred.promise;
    -                };
    -
    -                /**
    -                  Method called when the options button next to the root node is called.
    -                  The tree doesnt know about this, so it raises an event to tell the parent controller
    -                  about it.
    -                */
    -                scope.options = function(n, ev) {
    -                    emitEvent("treeOptionsClick", { element: elem, node: n, event: ev });
    -                };
    -
    -                /**
    -                  Method called when an item is clicked in the tree, this passes the
    -                  DOM element, the tree node object and the original click
    -                  and emits it as a treeNodeSelect element if there is a callback object
    -                  defined on the tree
    -                */
    -                scope.select = function (n, ev) {
    -                    //on tree select we need to remove the current node -
    -                    // whoever handles this will need to make sure the correct node is selected
    -                    //reset current node selection
    -                    scope.currentNode = null;
    -
    -                    emitEvent("treeNodeSelect", { element: elem, node: n, event: ev });
    -                };
    -
    -                scope.altSelect = function(n, ev) {
    -                    emitEvent("treeNodeAltSelect", { element: elem, tree: scope.tree, node: n, event: ev });
    -                };
    +            vm.clickItem = clickItem;
    +            vm.selectItem = selectItem;
     
    -                //watch for section changes
    -                scope.$watch("section", function(newVal, oldVal) {
     
    -                    if (!scope.tree) {
    -                        loadTree();
    -                    }
    +            function clickItem(item, $event, $index){
    +                // do magic here
    +            }
     
    -                    if (!newVal) {
    -                        //store the last section loaded
    -                        lastSection = oldVal;
    -                    }
    -                    else if (newVal !== oldVal && newVal !== lastSection) {
    -                        //only reload the tree data and Dom if the newval is different from the old one
    -                        // and if the last section loaded is different from the requested one.
    -                        loadTree();
    -
    -                        //store the new section to be loaded as the last section
    -                        //clear any active trees to reset lookups
    -                        lastSection = newVal;
    -                    }
    -                });
    +            function selectItem(item, $event, $index) {
    +                // set item.selected = true; to select the item
    +                // do magic here
    +            }
     
    -                setupExternalEvents();
    -                loadTree();
    -            };
             }
    -    };
    -}
    -
    -angular.module("umbraco.directives").directive('umbTree', umbTreeDirective);
    -
    -/**
    - * @ngdoc directive
    - * @name umbraco.directives.directive:umbTreeItem
    - * @element li
    - * @function
    - *
    - * @description
    - * Renders a list item, representing a single node in the tree.
    - * Includes element to toggle children, and a menu toggling button
    - *
    - * **note:** This directive is only used internally in the umbTree directive
    - *
    - * @example
    -   
    -    
    -         
    -    
    -   
    - */
    -angular.module("umbraco.directives")
    -.directive('umbTreeItem', function ($compile, $http, $templateCache, $interpolate, $log, $location, $rootScope, $window, treeService, $timeout, localizationService) {
    -    return {
    -        restrict: 'E',
    -        replace: true,
    -
    -        scope: {
    -            section: '@',
    -            eventhandler: '=',
    -            currentNode: '=',
    -            enablelistviewexpand: '@',
    -            node: '=',
    -            tree: '='
    -        },
    -
    -        //TODO: Remove more of the binding from this template and move the DOM manipulation to be manually done in the link function,
    -        // this will greatly improve performance since there's potentially a lot of nodes being rendered = a LOT of watches!
    -
    -        template: '
  • ' + - '
    ' + - //NOTE: This ins element is used to display the search icon if the node is a container/listview and the tree is currently in dialog - //'' + - ' ' + - '' + - '' + - //NOTE: These are the 'option' elipses - '' + - '
    ' + - '
    ' + - '
  • ', - - link: function (scope, element, attrs) { - localizationService.localize("general_search").then(function (value) { - scope.searchAltText = value; - }); - - //flag to enable/disable delete animations, default for an item is true - var deleteAnimations = true; + angular.module("umbraco").controller("My.Controller", Controller); + })(); +
    + +@param {array} content (binding): Array of content items. +@param {array=} contentProperties (binding): Array of content item properties to include in the item. If left empty the item will only show the item icon and name. +@param {callback=} onClick (binding): Callback method to handle click events on the content item. +

    The callback returns:

    +
      +
    • item: The clicked item
    • +
    • $event: The select event
    • +
    • $index: The item index
    • +
    +@param {callback=} onClickName (binding): Callback method to handle click events on the checkmark icon. +

    The callback returns:

    +
      +
    • item: The selected item
    • +
    • $event: The select event
    • +
    • $index: The item index
    • +
    +**/ + (function () { + 'use strict'; + function ContentGridDirective() { + function link(scope, el, attr, ctrl) { + scope.clickItem = function (item, $event, $index) { + if (scope.onClick) { + scope.onClick(item, $event, $index); + } + }; + scope.clickItemName = function (item, $event, $index) { + if (scope.onClickName) { + scope.onClickName(item, $event, $index); + } + }; + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-content-grid.html', + scope: { + content: '=', + contentProperties: '=', + onClick: '=', + onClickName: '=' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbContentGrid', ContentGridDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbDateTimePicker +@restrict E +@scope + +@description +Added in Umbraco version 7.6 +This directive is a wrapper of the bootstrap datetime picker version 3.1.3. Use it to render a date time picker. +For extra details about options and events take a look here: http://eonasdan.github.io/bootstrap-datetimepicker/ + +Use this directive to render a date time picker + +

    Markup example

    +
    +	
    + + + + +
    +
    + +

    Controller example

    +
    +	(function () {
    +		"use strict";
    +
    +		function Controller() {
    +
    +            var vm = this;
    +
    +            vm.date = "";
    +
    +            vm.config = {
    +                pickDate: true,
    +                pickTime: true,
    +                useSeconds: true,
    +                format: "YYYY-MM-DD HH:mm:ss",
    +                icons: {
    +                    time: "icon-time",
    +                    date: "icon-calendar",
    +                    up: "icon-chevron-up",
    +                    down: "icon-chevron-down"
    +                }
    +            };
     
    -            // Helper function to emit tree events
    -            function emitEvent(eventName, args) {
    +            vm.datePickerChange = datePickerChange;
    +            vm.datePickerError = datePickerError;
     
    -                if (scope.eventhandler) {
    -                    $(scope.eventhandler).trigger(eventName, args);
    +            function datePickerChange(event) {
    +                // handle change
    +                if(event.date && event.date.isValid()) {
    +                    var date = event.date.format(vm.datePickerConfig.format);
                     }
                 }
     
    -            // updates the node's DOM/styles
    -            function setupNodeDom(node, tree) {
    -                
    -                //get the first div element
    -                element.children(":first")
    -                    //set the padding
    -                    .css("padding-left", (node.level * 20) + "px");
    -
    -                //toggle visibility of last 'ins' depending on children
    -                //visibility still ensure the space is "reserved", so both nodes with and without children are aligned.
    -                
    -                if (node.hasChildren || node.metaData.isContainer && scope.enablelistviewexpand === "true") {
    -                    element.find("ins").last().css("visibility", "visible");
    -                }
    -                else {
    -                    element.find("ins").last().css("visibility", "hidden");
    -                }
    +            function datePickerError(event) {
    +                // handle error
    +            }
     
    -                var icon = element.find("i:first");
    -                icon.addClass(node.cssClass);
    -                icon.attr("title", node.routePath);
    +        }
     
    -                element.find("a:first").text(node.name);
    +		angular.module("umbraco").controller("My.Controller", Controller);
     
    -                if (!node.menuUrl) {
    -                    element.find("a.umb-options").remove();
    -                }
    +	})();
    +
    - if (node.style) { - element.find("i:first").attr("style", node.style); - } +@param {object} options (binding): Config object for the date picker. +@param {callback} onHide (callback): Hide callback. +@param {callback} onShow (callback): Show callback. +@param {callback} onChange (callback): Change callback. +@param {callback} onError (callback): Error callback. +@param {callback} onUpdate (callback): Update callback. +**/ + (function () { + 'use strict'; + function DateTimePickerDirective(assetsService) { + function link(scope, element, attrs, ctrl) { + function onInit() { + // load css file for the date picker + assetsService.loadCss('lib/datetimepicker/bootstrap-datetimepicker.min.css'); + // load the js file for the date picker + assetsService.loadJs('lib/datetimepicker/bootstrap-datetimepicker.js').then(function () { + // init date picker + initDatePicker(); + }); + } + function onHide(event) { + if (scope.onHide) { + scope.$apply(function () { + // callback + scope.onHide({ event: event }); + }); + } + } + function onShow() { + if (scope.onShow) { + scope.$apply(function () { + // callback + scope.onShow(); + }); + } + } + function onChange(event) { + if (scope.onChange && event.date && event.date.isValid()) { + scope.$apply(function () { + // callback + scope.onChange({ event: event }); + }); + } + } + function onError(event) { + if (scope.onError) { + scope.$apply(function () { + // callback + scope.onError({ event: event }); + }); + } + } + function onUpdate(event) { + if (scope.onUpdate) { + scope.$apply(function () { + // callback + scope.onUpdate({ event: event }); + }); + } + } + function initDatePicker() { + // Open the datepicker and add a changeDate eventlistener + element.datetimepicker(scope.options).on('dp.hide', onHide).on('dp.show', onShow).on('dp.change', onChange).on('dp.error', onError).on('dp.update', onUpdate); + } + onInit(); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-date-time-picker.html', + scope: { + options: '=', + onHide: '&', + onShow: '&', + onChange: '&', + onError: '&', + onUpdate: '&' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbDateTimePicker', DateTimePickerDirective); + }()); + (function () { + 'use strict'; + function UmbDisableFormValidation() { + var directive = { + restrict: 'A', + require: '?form', + link: function (scope, elm, attrs, ctrl) { + //override the $setValidity function of the form to disable validation + ctrl.$setValidity = function () { + }; + } + }; + return directive; + } + angular.module('umbraco.directives').directive('umbDisableFormValidation', UmbDisableFormValidation); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbDropdown +@restrict E +@scope + +@description +Added in versions 7.7.0: Use this component to render a dropdown menu. + +

    Markup example

    +
    +    
    + +
    + + + + + + + {{ item.name }} + + + +
    + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +
    +            vm.dropdownOpen = false;
    +            vm.items = [
    +                { "name": "Item 1" },
    +                { "name": "Item 2" },
    +                { "name": "Item 3" }
    +            ];
    +
    +            vm.toggle = toggle;
    +            vm.close = close;
    +            vm.select = select;
    +
    +            function toggle() {
    +                vm.dropdownOpen = true;
                 }
     
    -            //This will deleteAnimations to true after the current digest
    -            function enableDeleteAnimations() {
    -                //do timeout so that it re-enables them after this digest
    -                $timeout(function () {
    -                    //enable delete animations
    -                    deleteAnimations = true;
    -                }, 0, false);
    +            function close() {
    +                vm.dropdownOpen = false;
                 }
     
    -            /** Returns the css classses assigned to the node (div element) */
    -            scope.getNodeCssClass = function (node) {
    -                if (!node) {
    -                    return '';
    -                }
    -                var css = [];                
    -                if (node.cssClasses) {
    -                    _.each(node.cssClasses, function(c) {
    -                        css.push(c);
    -                    });
    -                }
    -                if (node.selected) {
    -                    css.push("umb-tree-node-checked");
    -                }
    -                return css.join(" ");
    -            };
    +            function select(item) {
    +                // Do your magic here
    +            }
     
    -            //add a method to the node which we can use to call to update the node data if we need to ,
    -            // this is done by sync tree, we don't want to add a $watch for each node as that would be crazy insane slow
    -            // so we have to do this
    -            scope.node.updateNodeData = function (newNode) {
    -                _.extend(scope.node, newNode);
    -                //now update the styles
    -                setupNodeDom(scope.node, scope.tree);
    -            };
    +        }
     
    -            /**
    -              Method called when the options button next to a node is called
    -              In the main tree this opens the menu, but internally the tree doesnt
    -              know about this, so it simply raises an event to tell the parent controller
    -              about it.
    -            */
    -            scope.options = function (n, ev) {
    -                emitEvent("treeOptionsClick", { element: element, tree: scope.tree, node: n, event: ev });
    -            };
    +        angular.module("umbraco").controller("MyDropdown.Controller", Controller);
    +    })();
    +
    - /** - Method called when an item is clicked in the tree, this passes the - DOM element, the tree node object and the original click - and emits it as a treeNodeSelect element if there is a callback object - defined on the tree - */ - scope.select = function (n, ev) { - if (ev.ctrlKey || - ev.shiftKey || - ev.metaKey || // apple - (ev.button && ev.button === 1) // middle click, >IE9 + everyone else - ) { - return; - } +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbDropdownItem umbDropdownItem}
    • +
    • {@link umbraco.directives.directive:umbKeyboardList umbKeyboardList}
    • +
    - emitEvent("treeNodeSelect", { element: element, tree: scope.tree, node: n, event: ev }); - ev.preventDefault(); - }; +@param {callback} onClose Callback when the dropdown menu closes. When you click outside or press esc. - /** - Method called when an item is right-clicked in the tree, this passes the - DOM element, the tree node object and the original click - and emits it as a treeNodeSelect element if there is a callback object - defined on the tree - */ - scope.altSelect = function (n, ev) { - emitEvent("treeNodeAltSelect", { element: element, tree: scope.tree, node: n, event: ev }); - }; +**/ + (function () { + 'use strict'; + function umbDropdown($document) { + function link(scope, element, attr, ctrl) { + scope.close = function () { + if (scope.onClose) { + scope.onClose(); + } + }; + // Handle keydown events + function keydown(event) { + // press escape + if (event.keyCode === 27) { + scope.onClose(); + } + } + // Stop to listen typing. + function stopListening() { + $document.off('keydown', keydown); + } + // Start listening to key typing. + $document.on('keydown', keydown); + // Stop listening when scope is destroyed. + scope.$on('$destroy', stopListening); + } + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/umb-dropdown.html', + scope: { onClose: '&' }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbDropdown', umbDropdown); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbDropdownItem +@restrict E - /** method to set the current animation for the node. - * This changes dynamically based on if we are changing sections or just loading normal tree data. - * When changing sections we don't want all of the tree-ndoes to do their 'leave' animations. - */ - scope.animation = function () { - if (scope.node.showHideAnimation) { - return scope.node.showHideAnimation; - } - if (deleteAnimations && scope.node.expanded) { - return { leave: 'tree-node-delete-leave' }; - } - else { - return {}; - } - }; +@description +Added in versions 7.7.0: Use this directive to construct a dropdown item. See documentation for {@link umbraco.directives.directive:umbDropdown umbDropdown}. - /** - Method called when a node in the tree is expanded, when clicking the arrow - takes the arrow DOM element and node data as parameters - emits treeNodeCollapsing event if already expanded and treeNodeExpanding if collapsed - */ - scope.load = function (node) { - if (node.expanded && !node.metaData.isContainer) { - deleteAnimations = false; - emitEvent("treeNodeCollapsing", { tree: scope.tree, node: node, element: element }); - node.expanded = false; - } - else { - scope.loadChildren(node, false); +**/ + (function () { + 'use strict'; + function umbDropdownItem() { + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/umb-dropdown-item.html' + }; + return directive; + } + angular.module('umbraco.directives').directive('umbDropdownItem', umbDropdownItem); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbEmptyState +@restrict E +@scope + +@description +Use this directive to show an empty state message. + +

    Markup example

    +
    +    
    + + + // Empty state content + + +
    +
    + +@param {string=} size Set the size of the text ("small", "large"). +@param {string=} position Set the position of the text ("center"). +**/ + (function () { + 'use strict'; + function EmptyStateDirective() { + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/umb-empty-state.html', + scope: { + size: '@', + position: '@' + } + }; + return directive; + } + angular.module('umbraco.directives').directive('umbEmptyState', EmptyStateDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbFolderGrid +@restrict E +@scope + +@description +Use this directive to generate a list of folders presented as a flexbox grid. + +

    Markup example

    +
    +    
    + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller(myService) {
    +
    +            var vm = this;
    +            vm.folders = [
    +                {
    +                    "name": "Folder 1",
    +                    "icon": "icon-folder",
    +                    "selected": false
    +                },
    +                {
    +                    "name": "Folder 2",
    +                    "icon": "icon-folder",
    +                    "selected": false
                     }
    -            };
     
    -            /* helper to force reloading children of a tree node */
    -            scope.loadChildren = function (node, forceReload) {
    -                //emit treeNodeExpanding event, if a callback object is set on the tree
    -                emitEvent("treeNodeExpanding", { tree: scope.tree, node: node });
    -
    -                if (node.hasChildren && (forceReload || !node.children || (angular.isArray(node.children) && node.children.length === 0))) {
    -                    //get the children from the tree service
    -                    treeService.loadNodeChildren({ node: node, section: scope.section })
    -                        .then(function (data) {
    -                            //emit expanded event
    -                            emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: data });
    -                            enableDeleteAnimations();
    -                        });
    -                }
    -                else {
    -                    emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: node.children });
    -                    node.expanded = true;
    -                    enableDeleteAnimations();
    -                }
    -            };            
    +            ];
    +
    +            vm.clickFolder = clickFolder;
    +            vm.selectFolder = selectFolder;
     
    -            //if the current path contains the node id, we will auto-expand the tree item children
    +            myService.getFolders().then(function(folders){
    +                vm.folders = folders;
    +            });
     
    -            setupNodeDom(scope.node, scope.tree);
    +            function clickFolder(folder){
    +                // Execute when clicking folder name/link
    +            }
     
    -            var template = '
    '; - var newElement = angular.element(template); - $compile(newElement)(scope); - element.append(newElement); + function selectFolder(folder, event, index) { + // Execute when clicking folder + // set folder.selected = true; to show checkmark icon + } } - }; -}); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTreeSearchBox -* @function -* @element ANY -* @restrict E + + angular.module("umbraco").controller("My.Controller", Controller); + })(); +
    + +@param {array} folders (binding): Array of folders +@param {callback=} onClick (binding): Callback method to handle click events on the folder. +

    The callback returns:

    +
      +
    • folder: The selected folder
    • +
    +@param {callback=} onSelect (binding): Callback method to handle click events on the checkmark icon. +

    The callback returns:

    +
      +
    • folder: The selected folder
    • +
    • $event: The select event
    • +
    • $index: The folder index
    • +
    **/ -function treeSearchBox(localizationService, searchService, $q) { - return { - scope: { - searchFromId: "@", - searchFromName: "@", - showSearch: "@", - section: "@", - hideSearchCallback: "=", - searchCallback: "=" - }, - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/components/tree/umb-tree-search-box.html', - link: function (scope, element, attrs, ctrl) { - - scope.term = ""; - scope.hideSearch = function() { - scope.term = ""; - scope.hideSearchCallback(); - }; - - localizationService.localize("general_typeToSearch").then(function (value) { - scope.searchPlaceholderText = value; - }); - - if (!scope.showSearch) { - scope.showSearch = "false"; - } - - //used to cancel any request in progress if another one needs to take it's place - var canceler = null; - - function performSearch() { - if (scope.term) { - scope.results = []; - - //a canceler exists, so perform the cancelation operation and reset - if (canceler) { - canceler.resolve(); - canceler = $q.defer(); - } - else { - canceler = $q.defer(); - } - - var searchArgs = { - term: scope.term, - canceler: canceler - }; - - //append a start node context if there is one - if (scope.searchFromId) { - searchArgs["searchFrom"] = scope.searchFromId; - } - - searcher(searchArgs).then(function (data) { - scope.searchCallback(data); - //set back to null so it can be re-created - canceler = null; - }); - } - } - - scope.$watch("term", _.debounce(function(newVal, oldVal) { - scope.$apply(function() { - if (newVal !== null && newVal !== undefined && newVal !== oldVal) { - performSearch(); + (function () { + 'use strict'; + function FolderGridDirective() { + function link(scope, el, attr, ctrl) { + scope.clickFolder = function (folder, $event, $index) { + if (scope.onClick) { + scope.onClick(folder, $event, $index); + $event.stopPropagation(); } - }); - }, 200)); - - var searcher = searchService.searchContent; - //search - if (scope.section === "member") { - searcher = searchService.searchMembers; - } - else if (scope.section === "media") { - searcher = searchService.searchMedia; + }; + scope.clickFolderName = function (folder, $event, $index) { + if (scope.onClickName) { + scope.onClickName(folder, $event, $index); + $event.stopPropagation(); + } + }; } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-folder-grid.html', + scope: { + folders: '=', + onClick: '=', + onClickName: '=' + }, + link: link + }; + return directive; } - }; -} -angular.module('umbraco.directives').directive("umbTreeSearchBox", treeSearchBox); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTreeSearchResults -* @function -* @element ANY -* @restrict E -**/ -function treeSearchResults() { - return { - scope: { - results: "=", - selectResultCallback: "=" - }, - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/components/tree/umb-tree-search-results.html', - link: function (scope, element, attrs, ctrl) { - - } - }; -} -angular.module('umbraco.directives').directive("umbTreeSearchResults", treeSearchResults); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbGenerateAlias -@restrict E -@scope - -@description -Use this directive to generate a camelCased umbraco alias. -When the aliasFrom value is changed the directive will get a formatted alias from the server and update the alias model. If "enableLock" is set to true -the directive will use {@link umbraco.directives.directive:umbLockedField umbLockedField} to lock and unlock the alias. - -

    Markup example

    -
    -    
    - - - - - - -
    -
    - -

    Controller example

    -
    -    (function () {
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -
    -            vm.name = "";
    -            vm.alias = "";
    -
    -        }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    - -@param {string} alias (binding): The model where the alias is bound. -@param {string} aliasFrom (binding): The model to generate the alias from. -@param {boolean=} enableLock (binding): Set to true to add a lock next to the alias from where it can be unlocked and changed. + angular.module('umbraco.directives').directive('umbFolderGrid', FolderGridDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbGenerateAlias +@restrict E +@scope + +@description +Use this directive to generate a camelCased umbraco alias. +When the aliasFrom value is changed the directive will get a formatted alias from the server and update the alias model. If "enableLock" is set to true +the directive will use {@link umbraco.directives.directive:umbLockedField umbLockedField} to lock and unlock the alias. + +

    Markup example

    +
    +    
    + + + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +
    +            vm.name = "";
    +            vm.alias = "";
    +
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +    })();
    +
    + +@param {string} alias (binding): The model where the alias is bound. +@param {string} aliasFrom (binding): The model to generate the alias from. +@param {boolean=} enableLock (binding): Set to true to add a lock next to the alias from where it can be unlocked and changed. **/ - -angular.module("umbraco.directives") - .directive('umbGenerateAlias', function ($timeout, entityResource) { + angular.module('umbraco.directives').directive('umbGenerateAlias', function ($timeout, entityResource) { return { restrict: 'E', templateUrl: 'views/components/umb-generate-alias.html', @@ -6397,3407 +7443,1640 @@ angular.module("umbraco.directives") serverValidationField: '@' }, link: function (scope, element, attrs, ctrl) { - var eventBindings = []; var bindWatcher = true; - var generateAliasTimeout = ""; + var generateAliasTimeout = ''; var updateAlias = false; - scope.locked = true; - scope.placeholderText = "Enter alias..."; - + scope.placeholderText = 'Enter alias...'; function generateAlias(value) { - - if (generateAliasTimeout) { - $timeout.cancel(generateAliasTimeout); - } - - if( value !== undefined && value !== "" && value !== null) { - - scope.alias = ""; - scope.placeholderText = "Generating Alias..."; - - generateAliasTimeout = $timeout(function () { - updateAlias = true; - entityResource.getSafeAlias(value, true).then(function (safeAlias) { - if (updateAlias) { - scope.alias = safeAlias.alias; - } - }); - }, 500); - - } else { - updateAlias = true; - scope.alias = ""; - scope.placeholderText = "Enter alias..."; - } - - } - + if (generateAliasTimeout) { + $timeout.cancel(generateAliasTimeout); + } + if (value !== undefined && value !== '' && value !== null) { + scope.alias = ''; + scope.placeholderText = 'Generating Alias...'; + generateAliasTimeout = $timeout(function () { + updateAlias = true; + entityResource.getSafeAlias(value, true).then(function (safeAlias) { + if (updateAlias) { + scope.alias = safeAlias.alias; + } + }); + }, 500); + } else { + updateAlias = true; + scope.alias = ''; + scope.placeholderText = 'Enter alias...'; + } + } // if alias gets unlocked - stop watching alias - eventBindings.push(scope.$watch('locked', function(newValue, oldValue){ - if(newValue === false) { - bindWatcher = false; + eventBindings.push(scope.$watch('locked', function (newValue, oldValue) { + if (newValue === false) { + bindWatcher = false; } })); - // validate custom entered alias - eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ - - if(scope.alias === "" && bindWatcher === true || scope.alias === null && bindWatcher === true) { - // add watcher - eventBindings.push(scope.$watch('aliasFrom', function(newValue, oldValue) { - if(bindWatcher) { - generateAlias(newValue); - } - })); - } - - })); - - // clean up - scope.$on('$destroy', function(){ - // unbind watchers - for(var e in eventBindings) { - eventBindings[e](); - } - }); - - } - }; - }); - -(function() { - 'use strict'; - - function AceEditorDirective(umbAceEditorConfig, assetsService, angularHelper) { - - /** - * Sets editor options such as the wrapping mode or the syntax checker. - * - * The supported options are: - * - *
      - *
    • showGutter
    • - *
    • useWrapMode
    • - *
    • onLoad
    • - *
    • theme
    • - *
    • mode
    • - *
    - * - * @param acee - * @param session ACE editor session - * @param {object} opts Options to be set - */ - var setOptions = function(acee, session, opts) { - - // sets the ace worker path, if running from concatenated - // or minified source - if (angular.isDefined(opts.workerPath)) { - var config = window.ace.require('ace/config'); - config.set('workerPath', opts.workerPath); - } - - // ace requires loading - if (angular.isDefined(opts.require)) { - opts.require.forEach(function(n) { - window.ace.require(n); + eventBindings.push(scope.$watch('alias', function (newValue, oldValue) { + if (scope.alias === '' && bindWatcher === true || scope.alias === null && bindWatcher === true) { + // add watcher + eventBindings.push(scope.$watch('aliasFrom', function (newValue, oldValue) { + if (bindWatcher) { + generateAlias(newValue); + } + })); + } + })); + // clean up + scope.$on('$destroy', function () { + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } }); } - - // Boolean options - if (angular.isDefined(opts.showGutter)) { - acee.renderer.setShowGutter(opts.showGutter); - } - if (angular.isDefined(opts.useWrapMode)) { - session.setUseWrapMode(opts.useWrapMode); - } - if (angular.isDefined(opts.showInvisibles)) { - acee.renderer.setShowInvisibles(opts.showInvisibles); - } - if (angular.isDefined(opts.showIndentGuides)) { - acee.renderer.setDisplayIndentGuides(opts.showIndentGuides); - } - if (angular.isDefined(opts.useSoftTabs)) { - session.setUseSoftTabs(opts.useSoftTabs); - } - if (angular.isDefined(opts.showPrintMargin)) { - acee.setShowPrintMargin(opts.showPrintMargin); - } - - // commands - if (angular.isDefined(opts.disableSearch) && opts.disableSearch) { - acee.commands.addCommands([{ - name: 'unfind', - bindKey: { - win: 'Ctrl-F', - mac: 'Command-F' - }, - exec: function() { - return false; - }, - readOnly: true - }]); - } - - // Basic options - if (angular.isString(opts.theme)) { - acee.setTheme('ace/theme/' + opts.theme); - } - if (angular.isString(opts.mode)) { - session.setMode('ace/mode/' + opts.mode); - } - // Advanced options - if (angular.isDefined(opts.firstLineNumber)) { - if (angular.isNumber(opts.firstLineNumber)) { - session.setOption('firstLineNumber', opts.firstLineNumber); - } else if (angular.isFunction(opts.firstLineNumber)) { - session.setOption('firstLineNumber', opts.firstLineNumber()); - } - } - - // advanced options - var key, obj; - if (angular.isDefined(opts.advanced)) { - for (key in opts.advanced) { - // create a javascript object with the key and value - obj = { - name: key, - value: opts.advanced[key] - }; - // try to assign the option to the ace editor - acee.setOption(obj.name, obj.value); - } - } - - // advanced options for the renderer - if (angular.isDefined(opts.rendererOptions)) { - for (key in opts.rendererOptions) { - // create a javascript object with the key and value - obj = { - name: key, - value: opts.rendererOptions[key] - }; - // try to assign the option to the ace editor - acee.renderer.setOption(obj.name, obj.value); - } - } - - // onLoad callbacks - angular.forEach(opts.callbacks, function(cb) { - if (angular.isFunction(cb)) { - cb(acee); - } - }); }; - - function link(scope, el, attr, ngModel) { - - // Load in ace library - assetsService.load(['lib/ace-builds/src-min-noconflict/ace.js', 'lib/ace-builds/src-min-noconflict/ext-language_tools.js']).then(function () { - if (angular.isUndefined(window.ace)) { - throw new Error('ui-ace need ace to work... (o rly?)'); + }); + (function () { + 'use strict'; + function GridSelector() { + function link(scope, el, attr, ctrl) { + var eventBindings = []; + scope.dialogModel = {}; + scope.showDialog = false; + scope.itemLabel = ''; + // set default item name + if (!scope.itemName) { + scope.itemLabel = 'item'; } else { - // init editor - init(); + scope.itemLabel = scope.itemName; } - }); - - function init() { - - /** - * Corresponds the umbAceEditorConfig ACE configuration. - * @type object - */ - var options = umbAceEditorConfig.ace || {}; - - /** - * umbAceEditorConfig merged with user options via json in attribute or data binding - * @type object - */ - var opts = angular.extend({}, options, scope.umbAceEditor); - - - //load ace libraries here... - - /** - * ACE editor - * @type object - */ - var acee = window.ace.edit(el[0]); - acee.$blockScrolling = Infinity; - - /** - * ACE editor session. - * @type object - * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session} - */ - var session = acee.getSession(); - - /** - * Reference to a change listener created by the listener factory. - * @function - * @see listenerFactory.onChange - */ - var onChangeListener; - - /** - * Reference to a blur listener created by the listener factory. - * @function - * @see listenerFactory.onBlur - */ - var onBlurListener; - - /** - * Calls a callback by checking its existing. The argument list - * is variable and thus this function is relying on the arguments - * object. - * @throws {Error} If the callback isn't a function - */ - var executeUserCallback = function() { - - /** - * The callback function grabbed from the array-like arguments - * object. The first argument should always be the callback. - * - * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} - * @type {*} - */ - var callback = arguments[0]; - - /** - * Arguments to be passed to the callback. These are taken - * from the array-like arguments object. The first argument - * is stripped because that should be the callback function. - * - * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} - * @type {Array} - */ - var args = Array.prototype.slice.call(arguments, 1); - - if (angular.isDefined(callback)) { - scope.$evalAsync(function() { - if (angular.isFunction(callback)) { - callback(args); - } else { - throw new Error('ui-ace use a function as callback.'); + scope.removeItem = function (selectedItem) { + var selectedItemIndex = scope.selectedItems.indexOf(selectedItem); + scope.selectedItems.splice(selectedItemIndex, 1); + }; + scope.removeDefaultItem = function () { + // it will be the last item so we can clear the array + scope.selectedItems = []; + // remove as default item + scope.defaultItem = null; + }; + scope.openItemPicker = function ($event) { + scope.dialogModel = { + view: 'itempicker', + title: 'Choose ' + scope.itemLabel, + availableItems: scope.availableItems, + selectedItems: scope.selectedItems, + event: $event, + show: true, + submit: function (model) { + scope.selectedItems.push(model.selectedItem); + // if no default item - set item as default + if (scope.defaultItem === null) { + scope.setAsDefaultItem(model.selectedItem); } - }); - } + scope.dialogModel.show = false; + scope.dialogModel = null; + } + }; }; - - - - /** - * Listener factory. Until now only change listeners can be created. - * @type object - */ - var listenerFactory = { - /** - * Creates a change listener which propagates the change event - * and the editor session to the callback from the user option - * onChange. It might be exchanged during runtime, if this - * happens the old listener will be unbound. - * - * @param callback callback function defined in the user options - * @see onChangeListener - */ - onChange: function(callback) { - return function(e) { - var newValue = session.getValue(); - angularHelper.safeApply(scope, function () { - scope.model = newValue; - }); - executeUserCallback(callback, e, acee); - }; - }, - /** - * Creates a blur listener which propagates the editor session - * to the callback from the user option onBlur. It might be - * exchanged during runtime, if this happens the old listener - * will be unbound. - * - * @param callback callback function defined in the user options - * @see onBlurListener - */ - onBlur: function(callback) { - return function() { - executeUserCallback(callback, acee); - }; - } + scope.setAsDefaultItem = function (selectedItem) { + // clear default item + scope.defaultItem = {}; + // set as default item + scope.defaultItem = selectedItem; }; - - attr.$observe('readonly', function(value) { - acee.setReadOnly(!!value || value === ''); - }); - - // Value Blind - if(scope.model) { - session.setValue(scope.model); - } - - // Listen for option updates - var updateOptions = function(current, previous) { - if (current === previous) { - return; + function updatePlaceholders() { + // update default item + if (scope.defaultItem !== null && scope.defaultItem.placeholder) { + scope.defaultItem.name = scope.name; + if (scope.alias !== null && scope.alias !== undefined) { + scope.defaultItem.alias = scope.alias; + } } - - opts = angular.extend({}, options, scope.umbAceEditor); - - opts.callbacks = [opts.onLoad]; - if (opts.onLoad !== options.onLoad) { - // also call the global onLoad handler - opts.callbacks.unshift(options.onLoad); - } - - // EVENTS - - // unbind old change listener - session.removeListener('change', onChangeListener); - - // bind new change listener - onChangeListener = listenerFactory.onChange(opts.onChange); - session.on('change', onChangeListener); - - // unbind old blur listener - //session.removeListener('blur', onBlurListener); - acee.removeListener('blur', onBlurListener); - - // bind new blur listener - onBlurListener = listenerFactory.onBlur(opts.onBlur); - acee.on('blur', onBlurListener); - - setOptions(acee, session, opts); - }; - - scope.$watch(scope.umbAceEditor, updateOptions, /* deep watch */ true); - - // set the options here, even if we try to watch later, if this - // line is missing things go wrong (and the tests will also fail) - updateOptions(options); - - el.on('$destroy', function() { - acee.session.$stopWorker(); - acee.destroy(); - }); - - scope.$watch(function() { - return [el[0].offsetWidth, el[0].offsetHeight]; - }, function() { - acee.resize(); - acee.renderer.updateFull(); - }, true); - - } - - } - - var directive = { - restrict: 'EA', - scope: { - "umbAceEditor": "=", - "model": "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives') - .constant('umbAceEditorConfig', {}) - .directive('umbAceEditor', AceEditorDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbAvatar -@restrict E -@scope - -@description -Use this directive to render an avatar. - -

    Markup example

    -
    -	
    - - - - -
    -
    - -

    Controller example

    -
    -	(function () {
    -		"use strict";
    -
    -		function Controller() {
    -
    -            var vm = this;
    -
    -            vm.avatar = [
    -                { value: "assets/logo.png" },
    -                { value: "assets/logo@2x.png" },
    -                { value: "assets/logo@3x.png" }
    -            ];
    -
    -        }
    -
    -		angular.module("umbraco").controller("My.Controller", Controller);
    -
    -	})();
    -
    - -@param {string} size (attribute): The size of the avatar (xs, s, m, l, xl). -@param {string} img-src (attribute): The image source to the avatar. -@param {string} img-srcset (atribute): Reponsive support for the image source. -**/ - -(function() { - 'use strict'; - - function AvatarDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-avatar.html', - scope: { - size: "@", - imgSrc: "@", - imgSrcset: "@" - } - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbAvatar', AvatarDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbChildSelector -@restrict E -@scope - -@description -Use this directive to render a ui component for selecting child items to a parent node. - -

    Markup example

    -
    -	
    - - - - - - - - -
    -
    - -

    Controller example

    -
    -	(function () {
    -		"use strict";
    -
    -		function Controller() {
    -
    -            var vm = this;
    -
    -            vm.id = 1;
    -            vm.name = "My Parent element";
    -            vm.icon = "icon-document";
    -            vm.selectedChildren = [];
    -            vm.availableChildren = [
    -                {
    -                    id: 1,
    -                    alias: "item1",
    -                    name: "Item 1",
    -                    icon: "icon-document"
    -                },
    -                {
    -                    id: 2,
    -                    alias: "item2",
    -                    name: "Item 2",
    -                    icon: "icon-document"
    +                    // update selected items
    +                    angular.forEach(scope.selectedItems, function (selectedItem) {
    +                        if (selectedItem.placeholder) {
    +                            selectedItem.name = scope.name;
    +                            if (scope.alias !== null && scope.alias !== undefined) {
    +                                selectedItem.alias = scope.alias;
    +                            }
    +                        }
    +                    });
    +                    // update availableItems
    +                    angular.forEach(scope.availableItems, function (availableItem) {
    +                        if (availableItem.placeholder) {
    +                            availableItem.name = scope.name;
    +                            if (scope.alias !== null && scope.alias !== undefined) {
    +                                availableItem.alias = scope.alias;
    +                            }
    +                        }
    +                    });
                     }
    -            ];
    -
    -            vm.addChild = addChild;
    -            vm.removeChild = removeChild;
    -
    -            function addChild($event) {
    -                vm.overlay = {
    -                    view: "itempicker",
    -                    title: "Choose child",
    -                    availableItems: vm.availableChildren,
    -                    selectedItems: vm.selectedChildren,
    -                    event: $event,
    -                    show: true,
    -                    submit: function(model) {
    -
    -                        // add selected child
    -                        vm.selectedChildren.push(model.selectedItem);
    -
    -                        // close overlay
    -                        vm.overlay.show = false;
    -                        vm.overlay = null;
    +                function activate() {
    +                    // add watchers for updating placeholde name and alias
    +                    if (scope.updatePlaceholder) {
    +                        eventBindings.push(scope.$watch('name', function (newValue, oldValue) {
    +                            updatePlaceholders();
    +                        }));
    +                        eventBindings.push(scope.$watch('alias', function (newValue, oldValue) {
    +                            updatePlaceholders();
    +                        }));
                         }
    -                };
    -            }
    -
    -            function removeChild($index) {
    -                vm.selectedChildren.splice($index, 1);
    +                }
    +                activate();
    +                // clean up
    +                scope.$on('$destroy', function () {
    +                    // clear watchers
    +                    for (var e in eventBindings) {
    +                        eventBindings[e]();
    +                    }
    +                });
                 }
    -
    -        }
    -
    -		angular.module("umbraco").controller("My.Controller", Controller);
    -
    -	})();
    -
    - -@param {array} selectedChildren (binding): Array of selected children. -@param {array} availableChildren (binding: Array of items available for selection. -@param {string} parentName (binding): The parent name. -@param {string} parentIcon (binding): The parent icon. -@param {number} parentId (binding): The parent id. -@param {callback} onRemove (binding): Callback when the remove button is clicked on an item. -

    The callback returns:

    -
      -
    • child: The selected item.
    • -
    • $index: The selected item index.
    • -
    -@param {callback} onAdd (binding): Callback when the add button is clicked. -

    The callback returns:

    -
      -
    • $event: The select event.
    • -
    -**/ - -(function() { - 'use strict'; - - function ChildSelectorDirective() { - - function link(scope, el, attr, ctrl) { - - var eventBindings = []; - scope.dialogModel = {}; - scope.showDialog = false; - - scope.removeChild = function(selectedChild, $index) { - if(scope.onRemove) { - scope.onRemove(selectedChild, $index); - } - }; - - scope.addChild = function($event) { - if(scope.onAdd) { - scope.onAdd($event); - } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-grid-selector.html', + scope: { + name: '=', + alias: '=', + selectedItems: '=', + availableItems: '=', + defaultItem: '=', + itemName: '@', + updatePlaceholder: '=' + }, + link: link }; - - function syncParentName() { - - // update name on available item - angular.forEach(scope.availableChildren, function(availableChild){ - if(availableChild.id === scope.parentId) { - availableChild.name = scope.parentName; - } - }); - - // update name on selected child - angular.forEach(scope.selectedChildren, function(selectedChild){ - if(selectedChild.id === scope.parentId) { - selectedChild.name = scope.parentName; - } - }); - - } - - function syncParentIcon() { - - // update icon on available item - angular.forEach(scope.availableChildren, function(availableChild){ - if(availableChild.id === scope.parentId) { - availableChild.icon = scope.parentIcon; - } - }); - - // update icon on selected child - angular.forEach(scope.selectedChildren, function(selectedChild){ - if(selectedChild.id === scope.parentId) { - selectedChild.icon = scope.parentIcon; - } - }); - - } - - eventBindings.push(scope.$watch('parentName', function(newValue, oldValue){ - - if (newValue === oldValue) { return; } - if ( oldValue === undefined || newValue === undefined) { return; } - - syncParentName(); - - })); - - eventBindings.push(scope.$watch('parentIcon', function(newValue, oldValue){ - - if (newValue === oldValue) { return; } - if ( oldValue === undefined || newValue === undefined) { return; } - - syncParentIcon(); - })); - - // clean up - scope.$on('$destroy', function(){ - // unbind watchers - for(var e in eventBindings) { - eventBindings[e](); - } - }); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-child-selector.html', - scope: { - selectedChildren: '=', - availableChildren: "=", - parentName: "=", - parentIcon: "=", - parentId: "=", - onRemove: "=", - onAdd: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbChildSelector', ChildSelectorDirective); - -})(); - -/** - * @ngdoc directive - * @name umbraco.directives.directive:umbConfirm - * @function - * @description - * A confirmation dialog - * - * @restrict E - */ -function confirmDirective() { - return { - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/components/umb-confirm.html', - scope: { - onConfirm: '=', - onCancel: '=', - caption: '@' - }, - link: function (scope, element, attr, ctrl) { - - } - }; -} -angular.module('umbraco.directives').directive("umbConfirm", confirmDirective); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbConfirmAction -@restrict E -@scope - -@description -

    Use this directive to toggle a confirmation prompt for an action. -The prompt consists of a checkmark and a cross to confirm or cancel the action. -The prompt can be opened in four direction up, down, left or right.

    - -

    Markup example

    -
    -    
    - -
    - - - -
    - -
    -
    - -

    Controller example

    -
    -    (function () {
    -
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -            vm.promptIsVisible = false;
    -
    -            vm.confirmAction = confirmAction;
    -            vm.showPrompt = showPrompt;
    -            vm.hidePrompt = hidePrompt;
    -
    -            function confirmAction() {
    -                // confirm logic here
    -            }
    -
    -            function showPrompt() {
    -                vm.promptIsVisible = true;
    -            }
    -
    -            function hidePrompt() {
    -                vm.promptIsVisible = false;
    -            }
    -
    +            return directive;
             }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    - -@param {string} direction The direction the prompt opens ("up", "down", "left", "right"). -@param {callback} onConfirm Callback when the checkmark is clicked. -@param {callback} onCancel Callback when the cross is clicked. -**/ - -(function() { - 'use strict'; - - function ConfirmAction() { - - function link(scope, el, attr, ctrl) { - - scope.clickConfirm = function() { - if(scope.onConfirm) { - scope.onConfirm(); - } - }; - - scope.clickCancel = function() { - if(scope.onCancel) { - scope.onCancel(); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-confirm-action.html', - scope: { - direction: "@", - onConfirm: "&", - onCancel: "&" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbConfirmAction', ConfirmAction); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbContentGrid -@restrict E -@scope - -@description -Use this directive to generate a list of content items presented as a flexbox grid. - -

    Markup example

    -
    -    
    - - - - -
    -
    - -

    Controller example

    -
    +        angular.module('umbraco.directives').directive('umbGridSelector', GridSelector);
    +    }());
         (function () {
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -            vm.contentItems = [
    -                {
    -                    "name": "Cape",
    -                    "published": true,
    -                    "icon": "icon-document",
    -                    "updateDate": "15-02-2016",
    -                    "owner": "Mr. Batman",
    -                    "selected": false
    -                },
    -                {
    -                    "name": "Utility Belt",
    -                    "published": true,
    -                    "icon": "icon-document",
    -                    "updateDate": "15-02-2016",
    -                    "owner": "Mr. Batman",
    -                    "selected": false
    -                },
    -                {
    -                    "name": "Cave",
    -                    "published": true,
    -                    "icon": "icon-document",
    -                    "updateDate": "15-02-2016",
    -                    "owner": "Mr. Batman",
    -                    "selected": false
    +        'use strict';
    +        function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService) {
    +            function link(scope, el, attr, ctrl) {
    +                var validationTranslated = '';
    +                var tabNoSortOrderTranslated = '';
    +                scope.sortingMode = false;
    +                scope.toolbar = [];
    +                scope.sortableOptionsGroup = {};
    +                scope.sortableOptionsProperty = {};
    +                scope.sortingButtonKey = 'general_reorder';
    +                function activate() {
    +                    setSortingOptions();
    +                    // set placeholder property on each group
    +                    if (scope.model.groups.length !== 0) {
    +                        angular.forEach(scope.model.groups, function (group) {
    +                            addInitProperty(group);
    +                        });
    +                    }
    +                    // add init tab
    +                    addInitGroup(scope.model.groups);
    +                    activateFirstGroup(scope.model.groups);
    +                    // localize texts
    +                    localizationService.localize('validation_validation').then(function (value) {
    +                        validationTranslated = value;
    +                    });
    +                    localizationService.localize('contentTypeEditor_tabHasNoSortOrder').then(function (value) {
    +                        tabNoSortOrderTranslated = value;
    +                    });
                     }
    -            ];
    -            vm.includeProperties = [
    -                {
    -                  "alias": "updateDate",
    -                  "header": "Last edited"
    -                },
    -                {
    -                  "alias": "owner",
    -                  "header": "Created by"
    +                function setSortingOptions() {
    +                    scope.sortableOptionsGroup = {
    +                        distance: 10,
    +                        tolerance: 'pointer',
    +                        opacity: 0.7,
    +                        scroll: true,
    +                        cursor: 'move',
    +                        placeholder: 'umb-group-builder__group-sortable-placeholder',
    +                        zIndex: 6000,
    +                        handle: '.umb-group-builder__group-handle',
    +                        items: '.umb-group-builder__group-sortable',
    +                        start: function (e, ui) {
    +                            ui.placeholder.height(ui.item.height());
    +                        },
    +                        stop: function (e, ui) {
    +                            updateTabsSortOrder();
    +                        }
    +                    };
    +                    scope.sortableOptionsProperty = {
    +                        distance: 10,
    +                        tolerance: 'pointer',
    +                        connectWith: '.umb-group-builder__properties',
    +                        opacity: 0.7,
    +                        scroll: true,
    +                        cursor: 'move',
    +                        placeholder: 'umb-group-builder__property_sortable-placeholder',
    +                        zIndex: 6000,
    +                        handle: '.umb-group-builder__property-handle',
    +                        items: '.umb-group-builder__property-sortable',
    +                        start: function (e, ui) {
    +                            ui.placeholder.height(ui.item.height());
    +                        },
    +                        stop: function (e, ui) {
    +                            updatePropertiesSortOrder();
    +                        }
    +                    };
                     }
    -            ];
    -
    -            vm.clickItem = clickItem;
    -            vm.selectItem = selectItem;
    -
    -
    -            function clickItem(item, $event, $index){
    -                // do magic here
    -            }
    -
    -            function selectItem(item, $event, $index) {
    -                // set item.selected = true; to select the item
    -                // do magic here
    -            }
    -
    -        }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    - -@param {array} content (binding): Array of content items. -@param {array=} contentProperties (binding): Array of content item properties to include in the item. If left empty the item will only show the item icon and name. -@param {callback=} onClick (binding): Callback method to handle click events on the content item. -

    The callback returns:

    -
      -
    • item: The clicked item
    • -
    • $event: The select event
    • -
    • $index: The item index
    • -
    -@param {callback=} onClickName (binding): Callback method to handle click events on the checkmark icon. -

    The callback returns:

    -
      -
    • item: The selected item
    • -
    • $event: The select event
    • -
    • $index: The item index
    • -
    -**/ - -(function() { - 'use strict'; - - function ContentGridDirective() { - - function link(scope, el, attr, ctrl) { - - scope.clickItem = function(item, $event, $index) { - if(scope.onClick) { - scope.onClick(item, $event, $index); - } - }; - - scope.clickItemName = function(item, $event, $index) { - if(scope.onClickName) { - scope.onClickName(item, $event, $index); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-content-grid.html', - scope: { - content: '=', - contentProperties: "=", - onClick: "=", - onClickName: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbContentGrid', ContentGridDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbDateTimePicker -@restrict E -@scope - -@description -Added in Umbraco version 7.6 -This directive is a wrapper of the bootstrap datetime picker version 3.1.3. Use it to render a date time picker. -For extra details about options and events take a look here: http://eonasdan.github.io/bootstrap-datetimepicker/ - -Use this directive to render a date time picker - -

    Markup example

    -
    -	
    - - - - -
    -
    - -

    Controller example

    -
    -	(function () {
    -		"use strict";
    -
    -		function Controller() {
    -
    -            var vm = this;
    -
    -            vm.date = "";
    -
    -            vm.config = {
    -                pickDate: true,
    -                pickTime: true,
    -                useSeconds: true,
    -                format: "YYYY-MM-DD HH:mm:ss",
    -                icons: {
    -                    time: "icon-time",
    -                    date: "icon-calendar",
    -                    up: "icon-chevron-up",
    -                    down: "icon-chevron-down"
    +                function updateTabsSortOrder() {
    +                    var first = true;
    +                    var prevSortOrder = 0;
    +                    scope.model.groups.map(function (group) {
    +                        var index = scope.model.groups.indexOf(group);
    +                        if (group.tabState !== 'init') {
    +                            // set the first not inherited tab to sort order 0
    +                            if (!group.inherited && first) {
    +                                // set the first tab sort order to 0 if prev is 0
    +                                if (prevSortOrder === 0) {
    +                                    group.sortOrder = 0;    // when the first tab is inherited and sort order is not 0
    +                                } else {
    +                                    group.sortOrder = prevSortOrder + 1;
    +                                }
    +                                first = false;
    +                            } else if (!group.inherited && !first) {
    +                                // find next group
    +                                var nextGroup = scope.model.groups[index + 1];
    +                                // if a groups is dropped in the middle of to groups with
    +                                // same sort order. Give it the dropped group same sort order
    +                                if (prevSortOrder === nextGroup.sortOrder) {
    +                                    group.sortOrder = prevSortOrder;
    +                                } else {
    +                                    group.sortOrder = prevSortOrder + 1;
    +                                }
    +                            }
    +                            // store this tabs sort order as reference for the next
    +                            prevSortOrder = group.sortOrder;
    +                        }
    +                    });
                     }
    -            };
    -
    -            vm.datePickerChange = datePickerChange;
    -            vm.datePickerError = datePickerError;
    -
    -            function datePickerChange(event) {
    -                // handle change
    -                if(event.date && event.date.isValid()) {
    -                    var date = event.date.format(vm.datePickerConfig.format);
    +                function filterAvailableCompositions(selectedContentType, selecting) {
    +                    //selecting = true if the user has check the item, false if the user has unchecked the item
    +                    var selectedContentTypeAliases = selecting ? //the user has selected the item so add to the current list
    +                    _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : //the user has unselected the item so remove from the current list
    +                    _.reject(scope.compositionsDialogModel.compositeContentTypes, function (i) {
    +                        return i === selectedContentType.alias;
    +                    });
    +                    //get the currently assigned property type aliases - ensure we pass these to the server side filer
    +                    var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function (g) {
    +                        return _.map(g.properties, function (p) {
    +                            return p.alias;
    +                        });
    +                    })), function (f) {
    +                        return f !== null && f !== undefined;
    +                    });
    +                    //use a different resource lookup depending on the content type type
    +                    var resourceLookup = scope.contentType === 'documentType' ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
    +                    return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) {
    +                        _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) {
    +                            //reset first
    +                            current.allowed = true;
    +                            //see if this list item is found in the response (allowed) list
    +                            var found = _.find(filteredAvailableCompositeTypes, function (f) {
    +                                return current.contentType.alias === f.contentType.alias;
    +                            });
    +                            //allow if the item was  found in the response (allowed) list -
    +                            // and ensure its set to allowed if it is currently checked,
    +                            // DO not allow if it's a locked content type.
    +                            current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 && selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1 || (found !== null && found !== undefined ? found.allowed : false);
    +                        });
    +                    });
                     }
    -            }
    -
    -            function datePickerError(event) {
    -                // handle error
    -            }
    -
    -        }
    -
    -		angular.module("umbraco").controller("My.Controller", Controller);
    -
    -	})();
    -
    - -@param {object} options (binding): Config object for the date picker. -@param {callback} onHide (callback): Hide callback. -@param {callback} onShow (callback): Show callback. -@param {callback} onChange (callback): Change callback. -@param {callback} onError (callback): Error callback. -@param {callback} onUpdate (callback): Update callback. -**/ - -(function () { - 'use strict'; - - function DateTimePickerDirective(assetsService) { - - function link(scope, element, attrs, ctrl) { - - function onInit() { - // load css file for the date picker - assetsService.loadCss('lib/datetimepicker/bootstrap-datetimepicker.min.css'); - - // load the js file for the date picker - assetsService.loadJs('lib/datetimepicker/bootstrap-datetimepicker.js').then(function () { - // init date picker - initDatePicker(); - }); - } - - function onHide(event) { - if (scope.onHide) { - scope.$apply(function(){ - // callback - scope.onHide({event: event}); + function updatePropertiesSortOrder() { + angular.forEach(scope.model.groups, function (group) { + if (group.tabState !== 'init') { + group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties); + } }); } - } - - function onShow() { - if (scope.onShow) { - scope.$apply(function(){ - // callback - scope.onShow(); + function setupAvailableContentTypesModel(result) { + scope.compositionsDialogModel.availableCompositeContentTypes = result; + //iterate each one and set it up + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) { + //enable it if it's part of the selected model + if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) { + c.allowed = true; + } + //set the inherited flags + c.inherited = false; + if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) { + c.inherited = true; + } + // convert icons for composite content types + iconHelper.formatContentTypeIcons([c.contentType]); + }); + } + /* ---------- DELETE PROMT ---------- */ + scope.togglePrompt = function (object) { + object.deletePrompt = !object.deletePrompt; + }; + scope.hidePrompt = function (object) { + object.deletePrompt = false; + }; + /* ---------- TOOLBAR ---------- */ + scope.toggleSortingMode = function (tool) { + if (scope.sortingMode === true) { + var sortOrderMissing = false; + for (var i = 0; i < scope.model.groups.length; i++) { + var group = scope.model.groups[i]; + if (group.tabState !== 'init' && group.sortOrder === undefined) { + sortOrderMissing = true; + group.showSortOrderMissing = true; + notificationsService.error(validationTranslated + ': ' + group.name + ' ' + tabNoSortOrderTranslated); + } + } + if (!sortOrderMissing) { + scope.sortingMode = false; + scope.sortingButtonKey = 'general_reorder'; + } + } else { + scope.sortingMode = true; + scope.sortingButtonKey = 'general_reorderDone'; + } + }; + scope.openCompositionsDialog = function () { + scope.compositionsDialogModel = { + title: 'Compositions', + contentType: scope.model, + compositeContentTypes: scope.model.compositeContentTypes, + view: 'views/common/overlays/contenttypeeditor/compositions/compositions.html', + confirmSubmit: { + title: 'Warning', + description: 'Removing a composition will delete all the associated property data. Once you save the document type there\'s no way back, are you sure?', + checkboxLabel: 'I know what I\'m doing', + enable: true + }, + submit: function (model, oldModel, confirmed) { + var compositionRemoved = false; + // check if any compositions has been removed + for (var i = 0; oldModel.compositeContentTypes.length > i; i++) { + var oldComposition = oldModel.compositeContentTypes[i]; + if (_.contains(model.compositeContentTypes, oldComposition) === false) { + compositionRemoved = true; + } + } + // show overlay confirm box if compositions has been removed. + if (compositionRemoved && confirmed === false) { + scope.compositionsDialogModel.confirmSubmit.show = true; // submit overlay if no compositions has been removed + // or the action has been confirmed + } else { + // make sure that all tabs has an init property + if (scope.model.groups.length !== 0) { + angular.forEach(scope.model.groups, function (group) { + addInitProperty(group); + }); + } + // remove overlay + scope.compositionsDialogModel.show = false; + scope.compositionsDialogModel = null; + } + }, + close: function (oldModel) { + // reset composition changes + scope.model.groups = oldModel.contentType.groups; + scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes; + // remove overlay + scope.compositionsDialogModel.show = false; + scope.compositionsDialogModel = null; + }, + selectCompositeContentType: function (selectedContentType) { + //first check if this is a new selection - we need to store this value here before any further digests/async + // because after that the scope.model.compositeContentTypes will be populated with the selected value. + var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1; + if (newSelection) { + //merge composition with content type + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === 'documentType' ? contentTypeResource.getById : mediaTypeResource.getById; + resourceLookup(selectedContentType.id).then(function (composition) { + //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and + // double check here. + var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); + if (overlappingAliases.length > 0) { + //this will create an invalid composition, need to uncheck it + scope.compositionsDialogModel.compositeContentTypes.splice(scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); + //dissallow this until something else is unchecked + selectedContentType.allowed = false; + } else { + contentTypeHelper.mergeCompositeContentType(scope.model, composition); + } + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + }); + }); + } else { + // split composition from content type + contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + }); + } + } + }; + var availableContentTypeResource = scope.contentType === 'documentType' ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; + var countContentTypeResource = scope.contentType === 'documentType' ? contentTypeResource.getCount : mediaTypeResource.getCount; + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function (g) { + return _.map(g.properties, function (p) { + return p.alias; + }); + })), function (f) { + return f !== null && f !== undefined; }); - } - } - - function onChange(event) { - if (scope.onChange && event.date && event.date.isValid()) { - scope.$apply(function(){ - // callback - scope.onChange({event: event}); + $q.all([ + //get available composite types + availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) { + setupAvailableContentTypesModel(result); + }), + //get content type count + countContentTypeResource().then(function (result) { + scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10); + }) + ]).then(function () { + //resolves when both other promises are done, now show it + scope.compositionsDialogModel.show = true; }); - } - } - - function onError(event) { - if (scope.onError) { - scope.$apply(function(){ - // callback - scope.onError({event:event}); + }; + /* ---------- GROUPS ---------- */ + scope.addGroup = function (group) { + // set group sort order + var index = scope.model.groups.indexOf(group); + var prevGroup = scope.model.groups[index - 1]; + if (index > 0) { + // set index to 1 higher than the previous groups sort order + group.sortOrder = prevGroup.sortOrder + 1; + } else { + // first group - sort order will be 0 + group.sortOrder = 0; + } + // activate group + scope.activateGroup(group); + }; + scope.activateGroup = function (selectedGroup) { + // set all other groups that are inactive to active + angular.forEach(scope.model.groups, function (group) { + // skip init tab + if (group.tabState !== 'init') { + group.tabState = 'inActive'; + } }); - } - } - - function onUpdate(event) { - if (scope.onUpdate) { - scope.$apply(function(){ - // callback - scope.onUpdate({event: event}); + selectedGroup.tabState = 'active'; + }; + scope.removeGroup = function (groupIndex) { + scope.model.groups.splice(groupIndex, 1); + addInitGroup(scope.model.groups); + }; + scope.updateGroupTitle = function (group) { + if (group.properties.length === 0) { + addInitProperty(group); + } + }; + scope.changeSortOrderValue = function (group) { + if (group.sortOrder !== undefined) { + group.showSortOrderMissing = false; + } + scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder'); + }; + function addInitGroup(groups) { + // check i init tab already exists + var addGroup = true; + angular.forEach(groups, function (group) { + if (group.tabState === 'init') { + addGroup = false; + } }); + if (addGroup) { + groups.push({ + properties: [], + parentTabContentTypes: [], + parentTabContentTypeNames: [], + name: '', + tabState: 'init' + }); + } + return groups; } - } - - function initDatePicker() { - // Open the datepicker and add a changeDate eventlistener - element - .datetimepicker(scope.options) - .on("dp.hide", onHide) - .on("dp.show", onShow) - .on("dp.change", onChange) - .on("dp.error", onError) - .on("dp.update", onUpdate); - } - - onInit(); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-date-time-picker.html', - scope: { - options: "=", - onHide: "&", - onShow: "&", - onChange: "&", - onError: "&", - onUpdate: "&" - }, - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbDateTimePicker', DateTimePickerDirective); - -})(); - -(function() { - 'use strict'; - - function UmbDisableFormValidation() { - - var directive = { - restrict: 'A', - require: '?form', - link: function (scope, elm, attrs, ctrl) { - //override the $setValidity function of the form to disable validation - ctrl.$setValidity = function () { }; - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbDisableFormValidation', UmbDisableFormValidation); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbEmptyState -@restrict E -@scope - -@description -Use this directive to show an empty state message. - -

    Markup example

    -
    -    
    - - - // Empty state content - - -
    -
    - -@param {string=} size Set the size of the text ("small", "large"). -@param {string=} position Set the position of the text ("center"). -**/ - -(function() { - 'use strict'; - - function EmptyStateDirective() { - - var directive = { - restrict: 'E', - replace: true, - transclude: true, - templateUrl: 'views/components/umb-empty-state.html', - scope: { - size: '@', - position: '@' - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEmptyState', EmptyStateDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbFolderGrid -@restrict E -@scope - -@description -Use this directive to generate a list of folders presented as a flexbox grid. - -

    Markup example

    -
    -    
    - - -
    -
    - -

    Controller example

    -
    -    (function () {
    -        "use strict";
    -
    -        function Controller(myService) {
    -
    -            var vm = this;
    -            vm.folders = [
    -                {
    -                    "name": "Folder 1",
    -                    "icon": "icon-folder",
    -                    "selected": false
    -                },
    -                {
    -                    "name": "Folder 2",
    -                    "icon": "icon-folder",
    -                    "selected": false
    +                function activateFirstGroup(groups) {
    +                    if (groups && groups.length > 0) {
    +                        var firstGroup = groups[0];
    +                        if (!firstGroup.tabState || firstGroup.tabState === 'inActive') {
    +                            firstGroup.tabState = 'active';
    +                        }
    +                    }
                     }
    -
    -            ];
    -
    -            vm.clickFolder = clickFolder;
    -            vm.selectFolder = selectFolder;
    -
    -            myService.getFolders().then(function(folders){
    -                vm.folders = folders;
    -            });
    -
    -            function clickFolder(folder){
    -                // Execute when clicking folder name/link
    -            }
    -
    -            function selectFolder(folder, event, index) {
    -                // Execute when clicking folder
    -                // set folder.selected = true; to show checkmark icon
    -            }
    -
    -        }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    - -@param {array} folders (binding): Array of folders -@param {callback=} onClick (binding): Callback method to handle click events on the folder. -

    The callback returns:

    -
      -
    • folder: The selected folder
    • -
    -@param {callback=} onSelect (binding): Callback method to handle click events on the checkmark icon. -

    The callback returns:

    -
      -
    • folder: The selected folder
    • -
    • $event: The select event
    • -
    • $index: The folder index
    • -
    -**/ - -(function() { - 'use strict'; - - function FolderGridDirective() { - - function link(scope, el, attr, ctrl) { - - scope.clickFolder = function(folder, $event, $index) { - if(scope.onClick) { - scope.onClick(folder, $event, $index); - $event.stopPropagation(); - } - }; - - scope.clickFolderName = function(folder, $event, $index) { - if(scope.onClickName) { - scope.onClickName(folder, $event, $index); - $event.stopPropagation(); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-folder-grid.html', - scope: { - folders: '=', - onClick: "=", - onClickName: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbFolderGrid', FolderGridDirective); - -})(); - -(function() { - 'use strict'; - - function GridSelector() { - - function link(scope, el, attr, ctrl) { - - var eventBindings = []; - scope.dialogModel = {}; - scope.showDialog = false; - scope.itemLabel = ""; - - // set default item name - if(!scope.itemName){ - scope.itemLabel = "item"; - } else { - scope.itemLabel = scope.itemName; - } - - scope.removeItem = function(selectedItem) { - var selectedItemIndex = scope.selectedItems.indexOf(selectedItem); - scope.selectedItems.splice(selectedItemIndex, 1); - }; - - scope.removeDefaultItem = function() { - - // it will be the last item so we can clear the array - scope.selectedItems = []; - - // remove as default item - scope.defaultItem = null; - - }; - - scope.openItemPicker = function($event){ - scope.dialogModel = { - view: "itempicker", - title: "Choose " + scope.itemLabel, - availableItems: scope.availableItems, - selectedItems: scope.selectedItems, - event: $event, - show: true, - submit: function(model) { - scope.selectedItems.push(model.selectedItem); - - // if no default item - set item as default - if(scope.defaultItem === null) { - scope.setAsDefaultItem(model.selectedItem); - } - - scope.dialogModel.show = false; - scope.dialogModel = null; + /* ---------- PROPERTIES ---------- */ + scope.addProperty = function (property, group) { + // set property sort order + var index = group.properties.indexOf(property); + var prevProperty = group.properties[index - 1]; + if (index > 0) { + // set index to 1 higher than the previous property sort order + property.sortOrder = prevProperty.sortOrder + 1; + } else { + // first property - sort order will be 0 + property.sortOrder = 0; } + // open property settings dialog + scope.editPropertyTypeSettings(property, group); }; - }; - - scope.setAsDefaultItem = function(selectedItem) { - - // clear default item - scope.defaultItem = {}; - - // set as default item - scope.defaultItem = selectedItem; - }; - - function updatePlaceholders() { - - // update default item - if(scope.defaultItem !== null && scope.defaultItem.placeholder) { - - scope.defaultItem.name = scope.name; - - if(scope.alias !== null && scope.alias !== undefined) { - scope.defaultItem.alias = scope.alias; - } - - } - - // update selected items - angular.forEach(scope.selectedItems, function(selectedItem) { - if(selectedItem.placeholder) { - - selectedItem.name = scope.name; - - if(scope.alias !== null && scope.alias !== undefined) { - selectedItem.alias = scope.alias; - } - - } - }); - - // update availableItems - angular.forEach(scope.availableItems, function(availableItem) { - if(availableItem.placeholder) { - - availableItem.name = scope.name; - - if(scope.alias !== null && scope.alias !== undefined) { - availableItem.alias = scope.alias; - } - - } - }); - - } - - function activate() { - - // add watchers for updating placeholde name and alias - if(scope.updatePlaceholder) { - eventBindings.push(scope.$watch('name', function(newValue, oldValue){ - updatePlaceholders(); - })); - - eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ - updatePlaceholders(); - })); - } - - } - - activate(); - - // clean up - scope.$on('$destroy', function(){ - - // clear watchers - for(var e in eventBindings) { - eventBindings[e](); - } - - }); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-grid-selector.html', - scope: { - name: "=", - alias: "=", - selectedItems: '=', - availableItems: "=", - defaultItem: "=", - itemName: "@", - updatePlaceholder: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbGridSelector', GridSelector); - -})(); - -(function() { - 'use strict'; - - function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService) { - - function link(scope, el, attr, ctrl) { - - var validationTranslated = ""; - var tabNoSortOrderTranslated = ""; - - scope.sortingMode = false; - scope.toolbar = []; - scope.sortableOptionsGroup = {}; - scope.sortableOptionsProperty = {}; - scope.sortingButtonKey = "general_reorder"; - - function activate() { - - setSortingOptions(); - - // set placeholder property on each group - if (scope.model.groups.length !== 0) { - angular.forEach(scope.model.groups, function(group) { - addInitProperty(group); - }); - } - - // add init tab - addInitGroup(scope.model.groups); - - activateFirstGroup(scope.model.groups); - - // localize texts - localizationService.localize("validation_validation").then(function(value) { - validationTranslated = value; - }); - - localizationService.localize("contentTypeEditor_tabHasNoSortOrder").then(function(value) { - tabNoSortOrderTranslated = value; - }); - } - - function setSortingOptions() { - - scope.sortableOptionsGroup = { - distance: 10, - tolerance: "pointer", - opacity: 0.7, - scroll: true, - cursor: "move", - placeholder: "umb-group-builder__group-sortable-placeholder", - zIndex: 6000, - handle: ".umb-group-builder__group-handle", - items: ".umb-group-builder__group-sortable", - start: function(e, ui) { - ui.placeholder.height(ui.item.height()); - }, - stop: function(e, ui) { - updateTabsSortOrder(); - }, - }; - - scope.sortableOptionsProperty = { - distance: 10, - tolerance: "pointer", - connectWith: ".umb-group-builder__properties", - opacity: 0.7, - scroll: true, - cursor: "move", - placeholder: "umb-group-builder__property_sortable-placeholder", - zIndex: 6000, - handle: ".umb-group-builder__property-handle", - items: ".umb-group-builder__property-sortable", - start: function(e, ui) { - ui.placeholder.height(ui.item.height()); - }, - stop: function(e, ui) { - updatePropertiesSortOrder(); - } - }; - - } - - function updateTabsSortOrder() { - - var first = true; - var prevSortOrder = 0; - - scope.model.groups.map(function(group){ - - var index = scope.model.groups.indexOf(group); - - if(group.tabState !== "init") { - - // set the first not inherited tab to sort order 0 - if(!group.inherited && first) { - - // set the first tab sort order to 0 if prev is 0 - if( prevSortOrder === 0 ) { - group.sortOrder = 0; - // when the first tab is inherited and sort order is not 0 - } else { - group.sortOrder = prevSortOrder + 1; - } - - first = false; - - } else if(!group.inherited && !first) { - - // find next group - var nextGroup = scope.model.groups[index + 1]; - - // if a groups is dropped in the middle of to groups with - // same sort order. Give it the dropped group same sort order - if( prevSortOrder === nextGroup.sortOrder ) { - group.sortOrder = prevSortOrder; - } else { - group.sortOrder = prevSortOrder + 1; - } - - } - - // store this tabs sort order as reference for the next - prevSortOrder = group.sortOrder; - - } - - }); - - } - - function filterAvailableCompositions(selectedContentType, selecting) { - - //selecting = true if the user has check the item, false if the user has unchecked the item - - var selectedContentTypeAliases = selecting ? - //the user has selected the item so add to the current list - _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : - //the user has unselected the item so remove from the current list - _.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) { - return i === selectedContentType.alias; - }); - - //get the currently assigned property type aliases - ensure we pass these to the server side filer - var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { - return _.map(g.properties, function(p) { - return p.alias; - }); - })), function (f) { - return f !== null && f !== undefined; - }); - - //use a different resource lookup depending on the content type type - var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; - - return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) { - _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { - //reset first - current.allowed = true; - //see if this list item is found in the response (allowed) list - var found = _.find(filteredAvailableCompositeTypes, function (f) { - return current.contentType.alias === f.contentType.alias; - }); - - //allow if the item was found in the response (allowed) list - - // and ensure its set to allowed if it is currently checked, - // DO not allow if it's a locked content type. - current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 && - (selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || ((found !== null && found !== undefined) ? found.allowed : false); - - }); - }); - } - - function updatePropertiesSortOrder() { - - angular.forEach(scope.model.groups, function(group){ - if( group.tabState !== "init" ) { - group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties); - } - }); - - } - - function setupAvailableContentTypesModel(result) { - scope.compositionsDialogModel.availableCompositeContentTypes = result; - //iterate each one and set it up - _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) { - //enable it if it's part of the selected model - if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) { - c.allowed = true; - } - - //set the inherited flags - c.inherited = false; - if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) { - c.inherited = true; - } - // convert icons for composite content types - iconHelper.formatContentTypeIcons([c.contentType]); - }); - } - - /* ---------- DELETE PROMT ---------- */ - - scope.togglePrompt = function (object) { - object.deletePrompt = !object.deletePrompt; - }; - - scope.hidePrompt = function (object) { - object.deletePrompt = false; - }; - - /* ---------- TOOLBAR ---------- */ - - scope.toggleSortingMode = function(tool) { - - if (scope.sortingMode === true) { - - var sortOrderMissing = false; - - for (var i = 0; i < scope.model.groups.length; i++) { - var group = scope.model.groups[i]; - if (group.tabState !== "init" && group.sortOrder === undefined) { - sortOrderMissing = true; - group.showSortOrderMissing = true; - notificationsService.error(validationTranslated + ": " + group.name + " " + tabNoSortOrderTranslated); - } - } - - if (!sortOrderMissing) { - scope.sortingMode = false; - scope.sortingButtonKey = "general_reorder"; - } - - } else { - - scope.sortingMode = true; - scope.sortingButtonKey = "general_reorderDone"; - - } - - }; - - scope.openCompositionsDialog = function() { - - scope.compositionsDialogModel = { - title: "Compositions", - contentType: scope.model, - compositeContentTypes: scope.model.compositeContentTypes, - view: "views/common/overlays/contenttypeeditor/compositions/compositions.html", - confirmSubmit: { - title: "Warning", - description: "Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?", - checkboxLabel: "I know what I'm doing", - enable: true - }, - submit: function(model, oldModel, confirmed) { - - var compositionRemoved = false; - - // check if any compositions has been removed - for(var i = 0; oldModel.compositeContentTypes.length > i; i++) { - - var oldComposition = oldModel.compositeContentTypes[i]; - - if(_.contains(model.compositeContentTypes, oldComposition) === false) { - compositionRemoved = true; - } - - } - - // show overlay confirm box if compositions has been removed. - if(compositionRemoved && confirmed === false) { - - scope.compositionsDialogModel.confirmSubmit.show = true; - - // submit overlay if no compositions has been removed - // or the action has been confirmed - } else { - - // make sure that all tabs has an init property - if (scope.model.groups.length !== 0) { - angular.forEach(scope.model.groups, function(group) { - addInitProperty(group); - }); + scope.editPropertyTypeSettings = function (property, group) { + if (!property.inherited && !property.locked) { + scope.propertySettingsDialogModel = {}; + scope.propertySettingsDialogModel.title = 'Property settings'; + scope.propertySettingsDialogModel.property = property; + scope.propertySettingsDialogModel.contentType = scope.contentType; + scope.propertySettingsDialogModel.contentTypeName = scope.model.name; + scope.propertySettingsDialogModel.view = 'views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html'; + scope.propertySettingsDialogModel.show = true; + // set state to active to access the preview + property.propertyState = 'active'; + // set property states + property.dialogIsOpen = true; + scope.propertySettingsDialogModel.submit = function (model) { + property.inherited = false; + property.dialogIsOpen = false; + // update existing data types + if (model.updateSameDataTypes) { + updateSameDataTypes(property); + } + // remove dialog + scope.propertySettingsDialogModel.show = false; + scope.propertySettingsDialogModel = null; + // push new init property to group + addInitProperty(group); + // set focus on init property + var numberOfProperties = group.properties.length; + group.properties[numberOfProperties - 1].focus = true; + // push new init tab to the scope + addInitGroup(scope.model.groups); + }; + scope.propertySettingsDialogModel.close = function (oldModel) { + // reset all property changes + property.label = oldModel.property.label; + property.alias = oldModel.property.alias; + property.description = oldModel.property.description; + property.config = oldModel.property.config; + property.editor = oldModel.property.editor; + property.view = oldModel.property.view; + property.dataTypeId = oldModel.property.dataTypeId; + property.dataTypeIcon = oldModel.property.dataTypeIcon; + property.dataTypeName = oldModel.property.dataTypeName; + property.validation.mandatory = oldModel.property.validation.mandatory; + property.validation.pattern = oldModel.property.validation.pattern; + property.showOnMemberProfile = oldModel.property.showOnMemberProfile; + property.memberCanEdit = oldModel.property.memberCanEdit; + // because we set state to active, to show a preview, we have to check if has been filled out + // label is required so if it is not filled we know it is a placeholder + if (oldModel.property.editor === undefined || oldModel.property.editor === null || oldModel.property.editor === '') { + property.propertyState = 'init'; + } else { + property.propertyState = oldModel.property.propertyState; + } + // remove dialog + scope.propertySettingsDialogModel.show = false; + scope.propertySettingsDialogModel = null; + }; } - - // remove overlay - scope.compositionsDialogModel.show = false; - scope.compositionsDialogModel = null; - } - - }, - close: function(oldModel) { - - // reset composition changes - scope.model.groups = oldModel.contentType.groups; - scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes; - - // remove overlay - scope.compositionsDialogModel.show = false; - scope.compositionsDialogModel = null; - - }, - selectCompositeContentType: function (selectedContentType) { - - //first check if this is a new selection - we need to store this value here before any further digests/async - // because after that the scope.model.compositeContentTypes will be populated with the selected value. - var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1; - - if (newSelection) { - //merge composition with content type - - //use a different resource lookup depending on the content type type - var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById; - - resourceLookup(selectedContentType.id).then(function (composition) { - //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and - // double check here. - var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); - if (overlappingAliases.length > 0) { - //this will create an invalid composition, need to uncheck it - scope.compositionsDialogModel.compositeContentTypes.splice( - scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); - //dissallow this until something else is unchecked - selectedContentType.allowed = false; - } - else { - contentTypeHelper.mergeCompositeContentType(scope.model, composition); - } - - //based on the selection, we need to filter the available composite types list - filterAvailableCompositions(selectedContentType, newSelection).then(function () { - //TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }; + scope.deleteProperty = function (tab, propertyIndex) { + // remove property + tab.properties.splice(propertyIndex, 1); + // if the last property in group is an placeholder - remove add new tab placeholder + if (tab.properties.length === 1 && tab.properties[0].propertyState === 'init') { + angular.forEach(scope.model.groups, function (group, index, groups) { + if (group.tabState === 'init') { + groups.splice(index, 1); + } }); + } + }; + function addInitProperty(group) { + var addInitPropertyBool = true; + var initProperty = { + label: null, + alias: null, + propertyState: 'init', + validation: { + mandatory: false, + pattern: null + } + }; + // check if there already is an init property + angular.forEach(group.properties, function (property) { + if (property.propertyState === 'init') { + addInitPropertyBool = false; + } }); - } - else { - // split composition from content type - contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); - - //based on the selection, we need to filter the available composite types list - filterAvailableCompositions(selectedContentType, newSelection).then(function () { - //TODO: Here we could probably re-enable selection if we previously showed a throbber or something + if (addInitPropertyBool) { + group.properties.push(initProperty); + } + return group; + } + function updateSameDataTypes(newProperty) { + // find each property + angular.forEach(scope.model.groups, function (group) { + angular.forEach(group.properties, function (property) { + if (property.dataTypeId === newProperty.dataTypeId) { + // update property data + property.config = newProperty.config; + property.editor = newProperty.editor; + property.view = newProperty.view; + property.dataTypeId = newProperty.dataTypeId; + property.dataTypeIcon = newProperty.dataTypeIcon; + property.dataTypeName = newProperty.dataTypeName; + } + }); }); } - - } - }; - - var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; - var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount; - - //get the currently assigned property type aliases - ensure we pass these to the server side filer - var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { - return _.map(g.properties, function(p) { - return p.alias; - }); - })), function(f) { - return f !== null && f !== undefined; - }); - $q.all([ - //get available composite types - availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) { - setupAvailableContentTypesModel(result); - }), - //get content type count - countContentTypeResource().then(function(result) { - scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10); - }) - ]).then(function() { - //resolves when both other promises are done, now show it - scope.compositionsDialogModel.show = true; - }); - - }; - - - /* ---------- GROUPS ---------- */ - - scope.addGroup = function(group) { - - // set group sort order - var index = scope.model.groups.indexOf(group); - var prevGroup = scope.model.groups[index - 1]; - - if( index > 0) { - // set index to 1 higher than the previous groups sort order - group.sortOrder = prevGroup.sortOrder + 1; - - } else { - // first group - sort order will be 0 - group.sortOrder = 0; - } - - // activate group - scope.activateGroup(group); - - }; - - scope.activateGroup = function(selectedGroup) { - - // set all other groups that are inactive to active - angular.forEach(scope.model.groups, function(group) { - // skip init tab - if (group.tabState !== "init") { - group.tabState = "inActive"; - } - }); - - selectedGroup.tabState = "active"; - - }; - - scope.removeGroup = function(groupIndex) { - scope.model.groups.splice(groupIndex, 1); - addInitGroup(scope.model.groups); - }; - - scope.updateGroupTitle = function(group) { - if (group.properties.length === 0) { - addInitProperty(group); - } - }; - - scope.changeSortOrderValue = function(group) { - - if (group.sortOrder !== undefined) { - group.showSortOrderMissing = false; - } - scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder'); - }; - - function addInitGroup(groups) { - - // check i init tab already exists - var addGroup = true; - - angular.forEach(groups, function(group) { - if (group.tabState === "init") { - addGroup = false; - } - }); - - if (addGroup) { - groups.push({ - properties: [], - parentTabContentTypes: [], - parentTabContentTypeNames: [], - name: "", - tabState: "init" - }); - } - - return groups; - } - - function activateFirstGroup(groups) { - if (groups && groups.length > 0) { - var firstGroup = groups[0]; - if(!firstGroup.tabState || firstGroup.tabState === "inActive") { - firstGroup.tabState = "active"; - } - } - } - - /* ---------- PROPERTIES ---------- */ - - scope.addProperty = function(property, group) { - - // set property sort order - var index = group.properties.indexOf(property); - var prevProperty = group.properties[index - 1]; - - if( index > 0) { - // set index to 1 higher than the previous property sort order - property.sortOrder = prevProperty.sortOrder + 1; - - } else { - // first property - sort order will be 0 - property.sortOrder = 0; - } - - // open property settings dialog - scope.editPropertyTypeSettings(property, group); - - }; - - scope.editPropertyTypeSettings = function(property, group) { - - if (!property.inherited && !property.locked) { - - scope.propertySettingsDialogModel = {}; - scope.propertySettingsDialogModel.title = "Property settings"; - scope.propertySettingsDialogModel.property = property; - scope.propertySettingsDialogModel.contentType = scope.contentType; - scope.propertySettingsDialogModel.contentTypeName = scope.model.name; - scope.propertySettingsDialogModel.view = "views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html"; - scope.propertySettingsDialogModel.show = true; - - // set state to active to access the preview - property.propertyState = "active"; - - // set property states - property.dialogIsOpen = true; - - scope.propertySettingsDialogModel.submit = function(model) { - - property.inherited = false; - property.dialogIsOpen = false; - - // update existing data types - if(model.updateSameDataTypes) { - updateSameDataTypes(property); - } - - // remove dialog - scope.propertySettingsDialogModel.show = false; - scope.propertySettingsDialogModel = null; - - // push new init property to group - addInitProperty(group); - - // set focus on init property - var numberOfProperties = group.properties.length; - group.properties[numberOfProperties - 1].focus = true; - - // push new init tab to the scope - addInitGroup(scope.model.groups); - - }; - - scope.propertySettingsDialogModel.close = function(oldModel) { - - // reset all property changes - property.label = oldModel.property.label; - property.alias = oldModel.property.alias; - property.description = oldModel.property.description; - property.config = oldModel.property.config; - property.editor = oldModel.property.editor; - property.view = oldModel.property.view; - property.dataTypeId = oldModel.property.dataTypeId; - property.dataTypeIcon = oldModel.property.dataTypeIcon; - property.dataTypeName = oldModel.property.dataTypeName; - property.validation.mandatory = oldModel.property.validation.mandatory; - property.validation.pattern = oldModel.property.validation.pattern; - property.showOnMemberProfile = oldModel.property.showOnMemberProfile; - property.memberCanEdit = oldModel.property.memberCanEdit; - - // because we set state to active, to show a preview, we have to check if has been filled out - // label is required so if it is not filled we know it is a placeholder - if(oldModel.property.editor === undefined || oldModel.property.editor === null || oldModel.property.editor === "") { - property.propertyState = "init"; - } else { - property.propertyState = oldModel.property.propertyState; - } - - // remove dialog - scope.propertySettingsDialogModel.show = false; - scope.propertySettingsDialogModel = null; - - }; - - } - }; - - scope.deleteProperty = function(tab, propertyIndex) { - - // remove property - tab.properties.splice(propertyIndex, 1); - - // if the last property in group is an placeholder - remove add new tab placeholder - if(tab.properties.length === 1 && tab.properties[0].propertyState === "init") { - - angular.forEach(scope.model.groups, function(group, index, groups){ - if(group.tabState === 'init') { - groups.splice(index, 1); - } - }); - - } - - }; - - function addInitProperty(group) { - - var addInitPropertyBool = true; - var initProperty = { - label: null, - alias: null, - propertyState: "init", - validation: { - mandatory: false, - pattern: null - } - }; - - // check if there already is an init property - angular.forEach(group.properties, function(property) { - if (property.propertyState === "init") { - addInitPropertyBool = false; - } - }); - - if (addInitPropertyBool) { - group.properties.push(initProperty); - } - - return group; - } - - function updateSameDataTypes(newProperty) { - - // find each property - angular.forEach(scope.model.groups, function(group){ - angular.forEach(group.properties, function(property){ - - if(property.dataTypeId === newProperty.dataTypeId) { - - // update property data - property.config = newProperty.config; - property.editor = newProperty.editor; - property.view = newProperty.view; - property.dataTypeId = newProperty.dataTypeId; - property.dataTypeIcon = newProperty.dataTypeIcon; - property.dataTypeName = newProperty.dataTypeName; - + var unbindModelWatcher = scope.$watch('model', function (newValue, oldValue) { + if (newValue !== undefined && newValue.groups !== undefined) { + activate(); + } + }); + // clean up + scope.$on('$destroy', function () { + unbindModelWatcher(); + }); } - - }); - }); - } - - - var unbindModelWatcher = scope.$watch('model', function(newValue, oldValue) { - if (newValue !== undefined && newValue.groups !== undefined) { - activate(); - } - }); - - // clean up - scope.$on('$destroy', function(){ - unbindModelWatcher(); - }); - - } - - var directive = { - restrict: "E", - replace: true, - templateUrl: "views/components/umb-groups-builder.html", - scope: { - model: "=", - compositions: "=", - sorting: "=", - contentType: "@" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbkeyboardShortcutsOverview -@restrict E -@scope - -@description - -

    Use this directive to show an overview of keyboard shortcuts in an editor. -The directive will render an overview trigger wich shows how the overview is opened. -When this combination is hit an overview is opened with shortcuts based on the model sent to the directive.

    - -

    Markup example

    -
    -    
    - - - - -
    -
    - -

    Controller example

    -
    -    (function () {
    -
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -
    -            vm.keyboardShortcutsOverview = [
    -                {
    -                    "name": "Sections",
    -                    "shortcuts": [
    -                        {
    -                            "description": "Navigate sections",
    -                            "keys": [
    -                                {"key": "1"},
    -                                {"key": "4"}
    -                            ],
    -                            "keyRange": true
    -                        }
    -                    ]
    +            var directive = {
    +                restrict: 'E',
    +                replace: true,
    +                templateUrl: 'views/components/umb-groups-builder.html',
    +                scope: {
    +                    model: '=',
    +                    compositions: '=',
    +                    sorting: '=',
    +                    contentType: '@'
                     },
    -                {
    -                    "name": "Design",
    -                    "shortcuts": [
    -                        {
    -                            "description": "Add tab",
    -                            "keys": [
    -                                {"key": "alt"},
    -                                {"key": "shift"},
    -                                {"key": "t"}
    -                            ]
    -                        }
    -                    ]
    -                }
    -            ];
    -
    +                link: link
    +            };
    +            return directive;
             }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    - -

    Model description

    -
      -
    • - name - (string) - - Sets the shortcut section name. -
    • -
    • - shortcuts - (array) - - Array of available shortcuts in the section. -
    • -
        -
      • - description - (string) - - Short description of the shortcut. -
      • -
      • - keys - (array) - - Array of keys in the shortcut. -
      • -
          -
        • - key - (string) - - The invidual key in the shortcut. -
        • -
        -
      • - keyRange - (boolean) - - Set to true to show a key range. It combines the shortcut keys with "-" instead of "+". -
      • -
      -
    - -@param {object} model keyboard shortcut model. See description and example above. + angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbkeyboardShortcutsOverview +@restrict E +@scope + +@description + +

    Use this directive to show an overview of keyboard shortcuts in an editor. +The directive will render an overview trigger wich shows how the overview is opened. +When this combination is hit an overview is opened with shortcuts based on the model sent to the directive.

    + +

    Markup example

    +
    +    
    + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +
    +            vm.keyboardShortcutsOverview = [
    +                {
    +                    "name": "Sections",
    +                    "shortcuts": [
    +                        {
    +                            "description": "Navigate sections",
    +                            "keys": [
    +                                {"key": "1"},
    +                                {"key": "4"}
    +                            ],
    +                            "keyRange": true
    +                        }
    +                    ]
    +                },
    +                {
    +                    "name": "Design",
    +                    "shortcuts": [
    +                        {
    +                            "description": "Add tab",
    +                            "keys": [
    +                                {"key": "alt"},
    +                                {"key": "shift"},
    +                                {"key": "t"}
    +                            ]
    +                        }
    +                    ]
    +                }
    +            ];
    +
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +    })();
    +
    + +

    Model description

    +
      +
    • + name + (string) - + Sets the shortcut section name. +
    • +
    • + shortcuts + (array) - + Array of available shortcuts in the section. +
    • +
        +
      • + description + (string) - + Short description of the shortcut. +
      • +
      • + keys + (array) - + Array of keys in the shortcut. +
      • +
          +
        • + key + (string) - + The invidual key in the shortcut. +
        • +
        +
      • + keyRange + (boolean) - + Set to true to show a key range. It combines the shortcut keys with "-" instead of "+". +
      • +
      +
    + +@param {object} model keyboard shortcut model. See description and example above. **/ - -(function () { - 'use strict'; - - function KeyboardShortcutsOverviewDirective(platformService) { - - function link(scope, el, attr, ctrl) { - - var eventBindings = []; - var isMac = platformService.isMac(); - - scope.toggleShortcutsOverlay = function () { - scope.showOverlay = !scope.showOverlay; - scope.onToggle(); - }; - - function onInit() { - - angular.forEach(scope.model, function (shortcutGroup) { - angular.forEach(shortcutGroup.shortcuts, function (shortcut) { - - shortcut.platformKeys = []; - - // get shortcut keys for mac - if (isMac && shortcut.keys && shortcut.keys.mac) { - shortcut.platformKeys = shortcut.keys.mac; - // get shortcut keys for windows - } else if (!isMac && shortcut.keys && shortcut.keys.win) { - shortcut.platformKeys = shortcut.keys.win; - // get default shortcut keys - } else if (shortcut.keys && shortcut && shortcut.keys.length > 0) { - shortcut.platformKeys = shortcut.keys; - } - + (function () { + 'use strict'; + function KeyboardShortcutsOverviewDirective(platformService) { + function link(scope, el, attr, ctrl) { + var eventBindings = []; + var isMac = platformService.isMac(); + scope.toggleShortcutsOverlay = function () { + scope.showOverlay = !scope.showOverlay; + scope.onToggle(); + }; + function onInit() { + angular.forEach(scope.model, function (shortcutGroup) { + angular.forEach(shortcutGroup.shortcuts, function (shortcut) { + shortcut.platformKeys = []; + // get shortcut keys for mac + if (isMac && shortcut.keys && shortcut.keys.mac) { + shortcut.platformKeys = shortcut.keys.mac; // get shortcut keys for windows + } else if (!isMac && shortcut.keys && shortcut.keys.win) { + shortcut.platformKeys = shortcut.keys.win; // get default shortcut keys + } else if (shortcut.keys && shortcut && shortcut.keys.length > 0) { + shortcut.platformKeys = shortcut.keys; + } + }); }); + } + onInit(); + eventBindings.push(scope.$watch('model', function (newValue, oldValue) { + if (newValue !== oldValue) { + onInit(); + } + })); + // clean up + scope.$on('$destroy', function () { + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } }); } - - onInit(); - - eventBindings.push(scope.$watch('model', function(newValue, oldValue){ - if (newValue !== oldValue) { - onInit(); - } - })); - - // clean up - scope.$on('$destroy', function () { - // unbind watchers - for (var e in eventBindings) { - eventBindings[e](); + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-keyboard-shortcuts-overview.html', + link: link, + scope: { + model: '=', + onToggle: '&', + showOverlay: '=?' } - }); - + }; + return directive; } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-keyboard-shortcuts-overview.html', - link: link, - scope: { - model: "=", - onToggle: "&", - showOverlay: "=?" - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbKeyboardShortcutsOverview', KeyboardShortcutsOverviewDirective); - -})(); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbLaunchMiniEditor -* @restrict E -* @function -* @description -* Used on a button to launch a mini content editor editor dialog + angular.module('umbraco.directives').directive('umbKeyboardShortcutsOverview', KeyboardShortcutsOverviewDirective); + }()); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbLaunchMiniEditor +* @restrict E +* @function +* @description +* Used on a button to launch a mini content editor editor dialog **/ -angular.module("umbraco.directives") - .directive('umbLaunchMiniEditor', function (miniEditorHelper) { + angular.module('umbraco.directives').directive('umbLaunchMiniEditor', function (miniEditorHelper) { return { restrict: 'A', replace: false, - scope: { - node: '=umbLaunchMiniEditor', - }, - link: function(scope, element, attrs) { - - element.click(function() { + scope: { node: '=umbLaunchMiniEditor' }, + link: function (scope, element, attrs) { + element.click(function () { miniEditorHelper.launchMiniEditor(scope.node); }); - } }; - }); -(function() { - 'use strict'; - - function LayoutSelectorDirective() { - - function link(scope, el, attr, ctrl) { - - scope.layoutDropDownIsOpen = false; - scope.showLayoutSelector = true; - - function activate() { - - setVisibility(); - - setActiveLayout(scope.layouts); - - } - - function setVisibility() { - - var numberOfAllowedLayouts = getNumberOfAllowedLayouts(scope.layouts); - - if(numberOfAllowedLayouts === 1) { - scope.showLayoutSelector = false; - } - - } - - function getNumberOfAllowedLayouts(layouts) { - - var allowedLayouts = 0; - - for (var i = 0; layouts.length > i; i++) { - - var layout = layouts[i]; - - if(layout.selected === true) { - allowedLayouts++; - } - - } - - return allowedLayouts; - } - - function setActiveLayout(layouts) { - - for (var i = 0; layouts.length > i; i++) { - var layout = layouts[i]; - if(layout.path === scope.activeLayout.path) { - layout.active = true; - } - } - - } - - scope.pickLayout = function(selectedLayout) { - if(scope.onLayoutSelect) { - scope.onLayoutSelect(selectedLayout); - scope.layoutDropDownIsOpen = false; - } - }; - - scope.toggleLayoutDropdown = function() { - scope.layoutDropDownIsOpen = !scope.layoutDropDownIsOpen; - }; - - scope.closeLayoutDropdown = function() { - scope.layoutDropDownIsOpen = false; - }; - - activate(); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-layout-selector.html', - scope: { - layouts: '=', - activeLayout: '=', - onLayoutSelect: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbLayoutSelector', LayoutSelectorDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbLightbox -@restrict E -@scope - -@description -

    Use this directive to open a gallery in a lightbox overlay.

    - -

    Markup example

    -
    -    
    - - - - - - -
    -
    - -

    Controller example

    -
    +    });
         (function () {
    -
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -
    -            vm.images = [
    -                {
    -                    "source": "linkToImage"
    +        'use strict';
    +        function LayoutSelectorDirective() {
    +            function link(scope, el, attr, ctrl) {
    +                scope.layoutDropDownIsOpen = false;
    +                scope.showLayoutSelector = true;
    +                function activate() {
    +                    setVisibility();
    +                    setActiveLayout(scope.layouts);
    +                }
    +                function setVisibility() {
    +                    var numberOfAllowedLayouts = getNumberOfAllowedLayouts(scope.layouts);
    +                    if (numberOfAllowedLayouts === 1) {
    +                        scope.showLayoutSelector = false;
    +                    }
    +                }
    +                function getNumberOfAllowedLayouts(layouts) {
    +                    var allowedLayouts = 0;
    +                    for (var i = 0; layouts.length > i; i++) {
    +                        var layout = layouts[i];
    +                        if (layout.selected === true) {
    +                            allowedLayouts++;
    +                        }
    +                    }
    +                    return allowedLayouts;
    +                }
    +                function setActiveLayout(layouts) {
    +                    for (var i = 0; layouts.length > i; i++) {
    +                        var layout = layouts[i];
    +                        if (layout.path === scope.activeLayout.path) {
    +                            layout.active = true;
    +                        }
    +                    }
    +                }
    +                scope.pickLayout = function (selectedLayout) {
    +                    if (scope.onLayoutSelect) {
    +                        scope.onLayoutSelect(selectedLayout);
    +                        scope.layoutDropDownIsOpen = false;
    +                    }
    +                };
    +                scope.toggleLayoutDropdown = function () {
    +                    scope.layoutDropDownIsOpen = !scope.layoutDropDownIsOpen;
    +                };
    +                scope.closeLayoutDropdown = function () {
    +                    scope.layoutDropDownIsOpen = false;
    +                };
    +                activate();
    +            }
    +            var directive = {
    +                restrict: 'E',
    +                replace: true,
    +                templateUrl: 'views/components/umb-layout-selector.html',
    +                scope: {
    +                    layouts: '=',
    +                    activeLayout: '=',
    +                    onLayoutSelect: '='
                     },
    -                {
    -                    "source": "linkToImage"
    -                }
    -            ]
    -
    -            vm.openLightbox = openLightbox;
    -            vm.closeLightbox = closeLightbox;
    -
    -            function openLightbox(itemIndex, items) {
    -                vm.lightbox = {
    -                    show: true,
    -                    items: items,
    -                    activeIndex: itemIndex
    +                link: link
    +            };
    +            return directive;
    +        }
    +        angular.module('umbraco.directives').directive('umbLayoutSelector', LayoutSelectorDirective);
    +    }());
    +    /**
    +@ngdoc directive
    +@name umbraco.directives.directive:umbLightbox
    +@restrict E
    +@scope
    +
    +@description
    +

    Use this directive to open a gallery in a lightbox overlay.

    + +

    Markup example

    +
    +    
    + + + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +
    +            vm.images = [
    +                {
    +                    "source": "linkToImage"
    +                },
    +                {
    +                    "source": "linkToImage"
    +                }
    +            ]
    +
    +            vm.openLightbox = openLightbox;
    +            vm.closeLightbox = closeLightbox;
    +
    +            function openLightbox(itemIndex, items) {
    +                vm.lightbox = {
    +                    show: true,
    +                    items: items,
    +                    activeIndex: itemIndex
    +                };
    +            }
    +
    +            function closeLightbox() {
    +                vm.lightbox.show = false;
    +                vm.lightbox = null;
    +            }
    +
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +    })();
    +
    + +@param {array} items Array of gallery items. +@param {callback} onClose Callback when the lightbox is closed. +@param {number} activeItemIndex Index of active item. +**/ + (function () { + 'use strict'; + function LightboxDirective() { + function link(scope, el, attr, ctrl) { + function activate() { + var eventBindings = []; + el.appendTo('body'); + // clean up + scope.$on('$destroy', function () { + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } + }); + } + scope.next = function () { + var nextItemIndex = scope.activeItemIndex + 1; + if (nextItemIndex < scope.items.length) { + scope.items[scope.activeItemIndex].active = false; + scope.items[nextItemIndex].active = true; + scope.activeItemIndex = nextItemIndex; + } + }; + scope.prev = function () { + var prevItemIndex = scope.activeItemIndex - 1; + if (prevItemIndex >= 0) { + scope.items[scope.activeItemIndex].active = false; + scope.items[prevItemIndex].active = true; + scope.activeItemIndex = prevItemIndex; + } + }; + scope.close = function () { + if (scope.onClose) { + scope.onClose(); + } }; + activate(); } - - function closeLightbox() { - vm.lightbox.show = false; - vm.lightbox = null; + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-lightbox.html', + scope: { + items: '=', + onClose: '=', + activeItemIndex: '=' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbLightbox', LightboxDirective); + }()); + (function () { + 'use strict'; + function ListViewLayoutDirective() { + function link(scope, el, attr, ctrl) { + scope.getContent = function (contentId) { + if (scope.onGetContent) { + scope.onGetContent(contentId); + } + }; } - + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-list-view-layout.html', + scope: { + contentId: '=', + folders: '=', + items: '=', + selection: '=', + options: '=', + entityType: '@', + onGetContent: '=' + }, + link: link + }; + return directive; } - - angular.module("umbraco").controller("My.Controller", Controller); - })(); -
    - -@param {array} items Array of gallery items. -@param {callback} onClose Callback when the lightbox is closed. -@param {number} activeItemIndex Index of active item. -**/ - - -(function() { - 'use strict'; - - function LightboxDirective() { - - function link(scope, el, attr, ctrl) { - - - function activate() { - - var eventBindings = []; - - el.appendTo("body"); - - // clean up - scope.$on('$destroy', function() { - // unbind watchers - for (var e in eventBindings) { - eventBindings[e](); + angular.module('umbraco.directives').directive('umbListViewLayout', ListViewLayoutDirective); + }()); + (function () { + 'use strict'; + function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper, listViewPrevalueHelper) { + function link(scope, el, attr, ctrl) { + scope.dataType = {}; + scope.editDataTypeSettings = false; + scope.customListViewCreated = false; + /* ---------- INIT ---------- */ + function activate() { + if (scope.enableListView) { + dataTypeResource.getByName(scope.listViewName).then(function (dataType) { + scope.dataType = dataType; + listViewPrevalueHelper.setPrevalues(dataType.preValues); + scope.customListViewCreated = checkForCustomListView(); + }); + } else { + scope.dataType = {}; + } + } + /* ----------- LIST VIEW SETTINGS --------- */ + scope.toggleEditListViewDataTypeSettings = function () { + scope.editDataTypeSettings = !scope.editDataTypeSettings; + }; + scope.saveListViewDataType = function () { + var preValues = dataTypeHelper.createPreValueProps(scope.dataType.preValues); + dataTypeResource.save(scope.dataType, preValues, false).then(function (dataType) { + // store data type + scope.dataType = dataType; + // hide settings panel + scope.editDataTypeSettings = false; + }); + }; + /* ---------- CUSTOM LIST VIEW ---------- */ + scope.createCustomListViewDataType = function () { + dataTypeResource.createCustomListView(scope.modelAlias).then(function (dataType) { + // store data type + scope.dataType = dataType; + // set list view name on scope + scope.listViewName = dataType.name; + // change state to custom list view + scope.customListViewCreated = true; + // show settings panel + scope.editDataTypeSettings = true; + }); + }; + scope.removeCustomListDataType = function () { + scope.editDataTypeSettings = false; + // delete custom list view data type + dataTypeResource.deleteById(scope.dataType.id).then(function (dataType) { + // set list view name on scope + if (scope.contentType === 'documentType') { + scope.listViewName = 'List View - Content'; + } else if (scope.contentType === 'mediaType') { + scope.listViewName = 'List View - Media'; + } + // get default data type + dataTypeResource.getByName(scope.listViewName).then(function (dataType) { + // store data type + scope.dataType = dataType; + // change state to default list view + scope.customListViewCreated = false; + }); + }); + }; + /* ----------- SCOPE WATCHERS ----------- */ + var unbindEnableListViewWatcher = scope.$watch('enableListView', function (newValue, oldValue) { + if (newValue !== undefined) { + activate(); } }); - } - - scope.next = function() { - - var nextItemIndex = scope.activeItemIndex + 1; - - if( nextItemIndex < scope.items.length) { - scope.items[scope.activeItemIndex].active = false; - scope.items[nextItemIndex].active = true; - scope.activeItemIndex = nextItemIndex; - } - }; - - scope.prev = function() { - - var prevItemIndex = scope.activeItemIndex - 1; - - if( prevItemIndex >= 0) { - scope.items[scope.activeItemIndex].active = false; - scope.items[prevItemIndex].active = true; - scope.activeItemIndex = prevItemIndex; - } - - }; - - scope.close = function() { - if(scope.onClose) { - scope.onClose(); + // clean up + scope.$on('$destroy', function () { + unbindEnableListViewWatcher(); + }); + /* ----------- METHODS ---------- */ + function checkForCustomListView() { + return scope.dataType.name === 'List View - ' + scope.modelAlias; } - }; - - activate(); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-lightbox.html', - scope: { - items: '=', - onClose: "=", - activeItemIndex: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbLightbox', LightboxDirective); - -})(); - -(function() { - 'use strict'; - - function ListViewLayoutDirective() { - - function link(scope, el, attr, ctrl) { - - scope.getContent = function(contentId) { - if(scope.onGetContent) { - scope.onGetContent(contentId); } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-list-view-layout.html', - scope: { - contentId: '=', - folders: '=', - items: '=', - selection: '=', - options: '=', - entityType: '@', - onGetContent: '=' - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbListViewLayout', ListViewLayoutDirective); - -})(); - -(function() { - 'use strict'; - - function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper, listViewPrevalueHelper) { - - function link(scope, el, attr, ctrl) { - - scope.dataType = {}; - scope.editDataTypeSettings = false; - scope.customListViewCreated = false; - - /* ---------- INIT ---------- */ - - function activate() { - - if(scope.enableListView) { - - dataTypeResource.getByName(scope.listViewName) - .then(function(dataType) { - - scope.dataType = dataType; - - listViewPrevalueHelper.setPrevalues(dataType.preValues); - scope.customListViewCreated = checkForCustomListView(); - - }); - - } else { - - scope.dataType = {}; - - } - - } - - /* ----------- LIST VIEW SETTINGS --------- */ - - scope.toggleEditListViewDataTypeSettings = function() { - scope.editDataTypeSettings = !scope.editDataTypeSettings; - }; - - scope.saveListViewDataType = function() { - - var preValues = dataTypeHelper.createPreValueProps(scope.dataType.preValues); - - dataTypeResource.save(scope.dataType, preValues, false).then(function(dataType) { - - // store data type - scope.dataType = dataType; - - // hide settings panel - scope.editDataTypeSettings = false; - - }); - - }; - - - /* ---------- CUSTOM LIST VIEW ---------- */ - - scope.createCustomListViewDataType = function() { - - dataTypeResource.createCustomListView(scope.modelAlias).then(function(dataType) { - - // store data type - scope.dataType = dataType; - - // set list view name on scope - scope.listViewName = dataType.name; - - // change state to custom list view - scope.customListViewCreated = true; - - // show settings panel - scope.editDataTypeSettings = true; - - }); - - }; - - scope.removeCustomListDataType = function() { - - scope.editDataTypeSettings = false; - - // delete custom list view data type - dataTypeResource.deleteById(scope.dataType.id).then(function(dataType) { - - // set list view name on scope - if(scope.contentType === "documentType") { - - scope.listViewName = "List View - Content"; - - } else if(scope.contentType === "mediaType") { - - scope.listViewName = "List View - Media"; - - } - - // get default data type - dataTypeResource.getByName(scope.listViewName) - .then(function(dataType) { - - // store data type - scope.dataType = dataType; - - // change state to default list view - scope.customListViewCreated = false; - - }); - }); - - }; - - /* ----------- SCOPE WATCHERS ----------- */ - var unbindEnableListViewWatcher = scope.$watch('enableListView', function(newValue, oldValue){ - - if(newValue !== undefined) { - activate(); - } - - }); - - // clean up - scope.$on('$destroy', function(){ - unbindEnableListViewWatcher(); - }); - - /* ----------- METHODS ---------- */ - - function checkForCustomListView() { - return scope.dataType.name === "List View - " + scope.modelAlias; - } - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-list-view-settings.html', - scope: { - enableListView: "=", - listViewName: "=", - modelAlias: "=", - contentType: "@" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbListViewSettings', ListViewSettingsDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbLoadIndicator -@restrict E - -@description -Use this directive to generate a loading indicator. - -

    Markup example

    -
    -    
    - - - - -
    -

    {{content}}

    -
    - -
    -
    - -

    Controller example

    -
    -    (function () {
    -        "use strict";
    -
    -        function Controller(myService) {
    -
    -            var vm = this;
    -
    -            vm.content = "";
    -            vm.loading = true;
    -
    -            myService.getContent().then(function(content){
    -                vm.content = content;
    -                vm.loading = false;
    -            });
    -
    +            var directive = {
    +                restrict: 'E',
    +                replace: true,
    +                templateUrl: 'views/components/umb-list-view-settings.html',
    +                scope: {
    +                    enableListView: '=',
    +                    listViewName: '=',
    +                    modelAlias: '=',
    +                    contentType: '@'
    +                },
    +                link: link
    +            };
    +            return directive;
             }
    -½
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    + angular.module('umbraco.directives').directive('umbListViewSettings', ListViewSettingsDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbLoadIndicator +@restrict E + +@description +Use this directive to generate a loading indicator. + +

    Markup example

    +
    +    
    + + + + +
    +

    {{content}}

    +
    + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller(myService) {
    +
    +            var vm = this;
    +
    +            vm.content = "";
    +            vm.loading = true;
    +
    +            myService.getContent().then(function(content){
    +                vm.content = content;
    +                vm.loading = false;
    +            });
    +
    +        }
    +½
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +    })();
    +
    **/ - -(function() { - 'use strict'; - - function UmbLoadIndicatorDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-load-indicator.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbLoadIndicator', UmbLoadIndicatorDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbLockedField -@restrict E -@scope - -@description -Use this directive to render a value with a lock next to it. When the lock is clicked the value gets unlocked and can be edited. - -

    Markup example

    -
    -	
    - - - - -
    -
    - -

    Controller example

    -
    -	(function () {
    -		"use strict";
    -
    -		function Controller() {
    -
    -			var vm = this;
    -			vm.value = "My locked text";
    -
    +    (function () {
    +        'use strict';
    +        function UmbLoadIndicatorDirective() {
    +            var directive = {
    +                restrict: 'E',
    +                replace: true,
    +                templateUrl: 'views/components/umb-load-indicator.html'
    +            };
    +            return directive;
             }
    -
    -		angular.module("umbraco").controller("My.Controller", Controller);
    -
    -	})();
    -
    - -@param {string} ngModel (binding): The locked text. -@param {boolean=} locked (binding): true by default. Set to false to unlock the text. -@param {string=} placeholderText (binding): If ngModel is empty this text will be shown. -@param {string=} regexValidation (binding): Set a regex expression for validation of the field. -@param {string=} serverValidationField (attribute): Set a server validation field. + angular.module('umbraco.directives').directive('umbLoadIndicator', UmbLoadIndicatorDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbLockedField +@restrict E +@scope + +@description +Use this directive to render a value with a lock next to it. When the lock is clicked the value gets unlocked and can be edited. + +

    Markup example

    +
    +	
    + + + + +
    +
    + +

    Controller example

    +
    +	(function () {
    +		"use strict";
    +
    +		function Controller() {
    +
    +			var vm = this;
    +			vm.value = "My locked text";
    +
    +        }
    +
    +		angular.module("umbraco").controller("My.Controller", Controller);
    +
    +	})();
    +
    + +@param {string} ngModel (binding): The locked text. +@param {boolean=} locked (binding): true by default. Set to false to unlock the text. +@param {string=} placeholderText (binding): If ngModel is empty this text will be shown. +@param {string=} regexValidation (binding): Set a regex expression for validation of the field. +@param {string=} serverValidationField (attribute): Set a server validation field. **/ - -(function() { - 'use strict'; - - function LockedFieldDirective($timeout, localizationService) { - - function link(scope, el, attr, ngModelCtrl) { - - function activate() { - - // if locked state is not defined as an attr set default state - if (scope.locked === undefined || scope.locked === null) { - scope.locked = true; - } - - // if regex validation is not defined as an attr set default state - // if this is set to an empty string then regex validation can be ignored. - if (scope.regexValidation === undefined || scope.regexValidation === null) { - scope.regexValidation = "^[a-zA-Z]\\w.*$"; - } - - if (scope.serverValidationField === undefined || scope.serverValidationField === null) { - scope.serverValidationField = ""; - } - - // if locked state is not defined as an attr set default state - if (scope.placeholderText === undefined || scope.placeholderText === null) { - scope.placeholderText = "Enter value..."; - } - - } - - scope.lock = function() { - scope.locked = true; - }; - - scope.unlock = function() { - scope.locked = false; - }; - - activate(); - - } - - var directive = { - require: "ngModel", - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-locked-field.html', - scope: { - ngModel: "=", - locked: "=?", - placeholderText: "=?", - regexValidation: "=?", - serverValidationField: "@" - }, - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbLockedField', LockedFieldDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbMediaGrid -@restrict E -@scope - -@description -Use this directive to generate a thumbnail grid of media items. - -

    Markup example

    -
    -    
    - - - - -
    -
    - -

    Controller example

    -
         (function () {
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -            vm.mediaItems = [];
    -
    -            vm.clickItem = clickItem;
    -            vm.clickItemName = clickItemName;
    -
    -            myService.getMediaItems().then(function (mediaItems) {
    -                vm.mediaItems = mediaItems;
    -            });
    -
    -            function clickItem(item, $event, $index){
    -                // do magic here
    -            }
    -
    -            function clickItemName(item, $event, $index) {
    -                // set item.selected = true; to select the item
    -                // do magic here
    +        'use strict';
    +        function LockedFieldDirective($timeout, localizationService) {
    +            function link(scope, el, attr, ngModelCtrl) {
    +                function activate() {
    +                    // if locked state is not defined as an attr set default state
    +                    if (scope.locked === undefined || scope.locked === null) {
    +                        scope.locked = true;
    +                    }
    +                    // if regex validation is not defined as an attr set default state
    +                    // if this is set to an empty string then regex validation can be ignored.
    +                    if (scope.regexValidation === undefined || scope.regexValidation === null) {
    +                        scope.regexValidation = '^[a-zA-Z]\\w.*$';
    +                    }
    +                    if (scope.serverValidationField === undefined || scope.serverValidationField === null) {
    +                        scope.serverValidationField = '';
    +                    }
    +                    // if locked state is not defined as an attr set default state
    +                    if (scope.placeholderText === undefined || scope.placeholderText === null) {
    +                        scope.placeholderText = 'Enter value...';
    +                    }
    +                }
    +                scope.lock = function () {
    +                    scope.locked = true;
    +                };
    +                scope.unlock = function () {
    +                    scope.locked = false;
    +                };
    +                activate();
                 }
    -
    +            var directive = {
    +                require: 'ngModel',
    +                restrict: 'E',
    +                replace: true,
    +                templateUrl: 'views/components/umb-locked-field.html',
    +                scope: {
    +                    ngModel: '=',
    +                    locked: '=?',
    +                    placeholderText: '=?',
    +                    regexValidation: '=?',
    +                    serverValidationField: '@'
    +                },
    +                link: link
    +            };
    +            return directive;
             }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    - -@param {array} items (binding): Array of media items. -@param {callback=} onDetailsHover (binding): Callback method when the details icon is hovered. -

    The callback returns:

    -
      -
    • item: The hovered item
    • -
    • $event: The hover event
    • -
    • hover: Boolean to tell if the item is hovered or not
    • -
    -@param {callback=} onClick (binding): Callback method to handle click events on the media item. -

    The callback returns:

    -
      -
    • item: The clicked item
    • -
    • $event: The click event
    • -
    • $index: The item index
    • -
    -@param {callback=} onClickName (binding): Callback method to handle click events on the media item name. -

    The callback returns:

    -
      -
    • item: The clicked item
    • -
    • $event: The click event
    • -
    • $index: The item index
    • -
    -@param {string=} filterBy (binding): String to filter media items by -@param {string=} itemMaxWidth (attribute): Sets a max width on the media item thumbnails. -@param {string=} itemMaxHeight (attribute): Sets a max height on the media item thumbnails. -@param {string=} itemMinWidth (attribute): Sets a min width on the media item thumbnails. -@param {string=} itemMinHeight (attribute): Sets a min height on the media item thumbnails. - + angular.module('umbraco.directives').directive('umbLockedField', LockedFieldDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbMediaGrid +@restrict E +@scope + +@description +Use this directive to generate a thumbnail grid of media items. + +

    Markup example

    +
    +    
    + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +            vm.mediaItems = [];
    +
    +            vm.clickItem = clickItem;
    +            vm.clickItemName = clickItemName;
    +
    +            myService.getMediaItems().then(function (mediaItems) {
    +                vm.mediaItems = mediaItems;
    +            });
    +
    +            function clickItem(item, $event, $index){
    +                // do magic here
    +            }
    +
    +            function clickItemName(item, $event, $index) {
    +                // set item.selected = true; to select the item
    +                // do magic here
    +            }
    +
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +    })();
    +
    + +@param {array} items (binding): Array of media items. +@param {callback=} onDetailsHover (binding): Callback method when the details icon is hovered. +

    The callback returns:

    +
      +
    • item: The hovered item
    • +
    • $event: The hover event
    • +
    • hover: Boolean to tell if the item is hovered or not
    • +
    +@param {callback=} onClick (binding): Callback method to handle click events on the media item. +

    The callback returns:

    +
      +
    • item: The clicked item
    • +
    • $event: The click event
    • +
    • $index: The item index
    • +
    +@param {callback=} onClickName (binding): Callback method to handle click events on the media item name. +

    The callback returns:

    +
      +
    • item: The clicked item
    • +
    • $event: The click event
    • +
    • $index: The item index
    • +
    +@param {string=} filterBy (binding): String to filter media items by +@param {string=} itemMaxWidth (attribute): Sets a max width on the media item thumbnails. +@param {string=} itemMaxHeight (attribute): Sets a max height on the media item thumbnails. +@param {string=} itemMinWidth (attribute): Sets a min width on the media item thumbnails. +@param {string=} itemMinHeight (attribute): Sets a min height on the media item thumbnails. + **/ - -(function() { - 'use strict'; - - function MediaGridDirective($filter, mediaHelper) { - - function link(scope, el, attr, ctrl) { - - var itemDefaultHeight = 200; - var itemDefaultWidth = 200; - var itemMaxWidth = 200; - var itemMaxHeight = 200; - var itemMinWidth = 125; - var itemMinHeight = 125; - - function activate() { - - if (scope.itemMaxWidth) { - itemMaxWidth = scope.itemMaxWidth; - } - - if (scope.itemMaxHeight) { - itemMaxHeight = scope.itemMaxHeight; - } - - if (scope.itemMinWidth) { - itemMinWidth = scope.itemMinWidth; - } - - if (scope.itemMinWidth) { - itemMinHeight = scope.itemMinHeight; - } - - for (var i = 0; scope.items.length > i; i++) { - var item = scope.items[i]; - setItemData(item); - setOriginalSize(item, itemMaxHeight); - - // remove non images when onlyImages is set to true - if(scope.onlyImages === "true" && !item.isFolder && !item.thumbnail){ - scope.items.splice(i, 1); - i--; - } - - } - - if (scope.items.length > 0) { - setFlexValues(scope.items); - } - - } - - function setItemData(item) { - - // check if item is a folder - if(item.image) { - // if is has an image path, it is not a folder - item.isFolder = false; - } else { - item.isFolder = !mediaHelper.hasFilePropertyType(item); - } - - if (!item.isFolder) { - - // handle entity - if(item.image) { - item.thumbnail = mediaHelper.resolveFileFromEntity(item, true); - item.extension = mediaHelper.getFileExtension(item.image); - // handle full media object - } else { - item.thumbnail = mediaHelper.resolveFile(item, true); - item.image = mediaHelper.resolveFile(item, false); - - var fileProp = _.find(item.properties, function (v) { - return (v.alias === "umbracoFile"); - }); - - if (fileProp && fileProp.value) { - item.file = fileProp.value; - } - - var extensionProp = _.find(item.properties, function (v) { - return (v.alias === "umbracoExtension"); - }); - - if (extensionProp && extensionProp.value) { - item.extension = extensionProp.value; + (function () { + 'use strict'; + function MediaGridDirective($filter, mediaHelper) { + function link(scope, el, attr, ctrl) { + var itemDefaultHeight = 200; + var itemDefaultWidth = 200; + var itemMaxWidth = 200; + var itemMaxHeight = 200; + var itemMinWidth = 125; + var itemMinHeight = 125; + function activate() { + if (scope.itemMaxWidth) { + itemMaxWidth = scope.itemMaxWidth; + } + if (scope.itemMaxHeight) { + itemMaxHeight = scope.itemMaxHeight; + } + if (scope.itemMinWidth) { + itemMinWidth = scope.itemMinWidth; + } + if (scope.itemMinHeight) { + itemMinHeight = scope.itemMinHeight; + } + for (var i = 0; scope.items.length > i; i++) { + var item = scope.items[i]; + setItemData(item); + setOriginalSize(item, itemMaxHeight); + // remove non images when onlyImages is set to true + if (scope.onlyImages === 'true' && !item.isFolder && !item.thumbnail) { + scope.items.splice(i, 1); + i--; } - + } + if (scope.items.length > 0) { + setFlexValues(scope.items); } } - } - - function setOriginalSize(item, maxHeight) { - - //set to a square by default - item.width = itemDefaultWidth; - item.height = itemDefaultHeight; - item.aspectRatio = 1; - - var widthProp = _.find(item.properties, function(v) { - return (v.alias === "umbracoWidth"); - }); - - if (widthProp && widthProp.value) { - item.width = parseInt(widthProp.value, 10); - if (isNaN(item.width)) { - item.width = itemDefaultWidth; + function setItemData(item) { + // check if item is a folder + if (item.image) { + // if is has an image path, it is not a folder + item.isFolder = false; + } else { + item.isFolder = !mediaHelper.hasFilePropertyType(item); + } + if (!item.isFolder) { + // handle entity + if (item.image) { + item.thumbnail = mediaHelper.resolveFileFromEntity(item, true); + item.extension = mediaHelper.getFileExtension(item.image); // handle full media object + } else { + item.thumbnail = mediaHelper.resolveFile(item, true); + item.image = mediaHelper.resolveFile(item, false); + var fileProp = _.find(item.properties, function (v) { + return v.alias === 'umbracoFile'; + }); + if (fileProp && fileProp.value) { + item.file = fileProp.value; + } + var extensionProp = _.find(item.properties, function (v) { + return v.alias === 'umbracoExtension'; + }); + if (extensionProp && extensionProp.value) { + item.extension = extensionProp.value; + } + } } } - - var heightProp = _.find(item.properties, function(v) { - return (v.alias === "umbracoHeight"); - }); - - if (heightProp && heightProp.value) { - item.height = parseInt(heightProp.value, 10); - if (isNaN(item.height)) { - item.height = itemDefaultWidth; - } - } - - item.aspectRatio = item.width / item.height; - - // set max width and height - // landscape - if (item.aspectRatio >= 1) { - if (item.width > itemMaxWidth) { - item.width = itemMaxWidth; - item.height = itemMaxWidth / item.aspectRatio; - } - // portrait - } else { - if (item.height > itemMaxHeight) { - item.height = itemMaxHeight; - item.width = itemMaxHeight * item.aspectRatio; + function setOriginalSize(item, maxHeight) { + //set to a square by default + item.width = itemDefaultWidth; + item.height = itemDefaultHeight; + item.aspectRatio = 1; + var widthProp = _.find(item.properties, function (v) { + return v.alias === 'umbracoWidth'; + }); + if (widthProp && widthProp.value) { + item.width = parseInt(widthProp.value, 10); + if (isNaN(item.width)) { + item.width = itemDefaultWidth; + } + } + var heightProp = _.find(item.properties, function (v) { + return v.alias === 'umbracoHeight'; + }); + if (heightProp && heightProp.value) { + item.height = parseInt(heightProp.value, 10); + if (isNaN(item.height)) { + item.height = itemDefaultWidth; + } + } + item.aspectRatio = item.width / item.height; + // set max width and height + // landscape + if (item.aspectRatio >= 1) { + if (item.width > itemMaxWidth) { + item.width = itemMaxWidth; + item.height = itemMaxWidth / item.aspectRatio; + } // portrait + } else { + if (item.height > itemMaxHeight) { + item.height = itemMaxHeight; + item.width = itemMaxHeight * item.aspectRatio; + } } } - - } - - function setFlexValues(mediaItems) { - - var flexSortArray = mediaItems; - var smallestImageWidth = null; - var widestImageAspectRatio = null; - - // sort array after image width with the widest image first - flexSortArray = $filter('orderBy')(flexSortArray, 'width', true); - - // find widest image aspect ratio - widestImageAspectRatio = flexSortArray[0].aspectRatio; - - // find smallest image width - smallestImageWidth = flexSortArray[flexSortArray.length - 1].width; - - for (var i = 0; flexSortArray.length > i; i++) { - - var mediaItem = flexSortArray[i]; - var flex = 1 / (widestImageAspectRatio / mediaItem.aspectRatio); - - if (flex === 0) { - flex = 1; - } - - var imageMinFlexWidth = smallestImageWidth * flex; - - var flexStyle = { - "flex": flex + " 1 " + imageMinFlexWidth + "px", - "max-width": mediaItem.width + "px", - "min-width": itemMinWidth + "px", - "min-height": itemMinHeight + "px" - }; - - mediaItem.flexStyle = flexStyle; - + function setFlexValues(mediaItems) { + var flexSortArray = mediaItems; + var smallestImageWidth = null; + var widestImageAspectRatio = null; + // sort array after image width with the widest image first + flexSortArray = $filter('orderBy')(flexSortArray, 'width', true); + // find widest image aspect ratio + widestImageAspectRatio = flexSortArray[0].aspectRatio; + // find smallest image width + smallestImageWidth = flexSortArray[flexSortArray.length - 1].width; + for (var i = 0; flexSortArray.length > i; i++) { + var mediaItem = flexSortArray[i]; + var flex = 1 / (widestImageAspectRatio / mediaItem.aspectRatio); + if (flex === 0) { + flex = 1; + } + var imageMinFlexWidth = smallestImageWidth * flex; + var flexStyle = { + 'flex': flex + ' 1 ' + imageMinFlexWidth + 'px', + 'max-width': mediaItem.width + 'px', + 'min-width': itemMinWidth + 'px', + 'min-height': itemMinHeight + 'px' + }; + mediaItem.flexStyle = flexStyle; + } } - + scope.clickItem = function (item, $event, $index) { + if (scope.onClick) { + scope.onClick(item, $event, $index); + $event.stopPropagation(); + } + }; + scope.clickItemName = function (item, $event, $index) { + if (scope.onClickName) { + scope.onClickName(item, $event, $index); + $event.stopPropagation(); + } + }; + scope.hoverItemDetails = function (item, $event, hover) { + if (scope.onDetailsHover) { + scope.onDetailsHover(item, $event, hover); + } + }; + var unbindItemsWatcher = scope.$watch('items', function (newValue, oldValue) { + if (angular.isArray(newValue)) { + activate(); + } + }); + scope.$on('$destroy', function () { + unbindItemsWatcher(); + }); } - - scope.clickItem = function(item, $event, $index) { - if (scope.onClick) { - scope.onClick(item, $event, $index); - $event.stopPropagation(); - } - }; - - scope.clickItemName = function(item, $event, $index) { - if (scope.onClickName) { - scope.onClickName(item, $event, $index); - $event.stopPropagation(); - } - }; - - scope.hoverItemDetails = function(item, $event, hover) { - if (scope.onDetailsHover) { - scope.onDetailsHover(item, $event, hover); - } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-media-grid.html', + scope: { + items: '=', + onDetailsHover: '=', + onClick: '=', + onClickName: '=', + filterBy: '=', + itemMaxWidth: '@', + itemMaxHeight: '@', + itemMinWidth: '@', + itemMinHeight: '@', + onlyImages: '@' + }, + link: link }; - - var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { - if (angular.isArray(newValue)) { - activate(); - } - }); - - scope.$on('$destroy', function() { - unbindItemsWatcher(); - }); - + return directive; } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-media-grid.html', - scope: { - items: '=', - onDetailsHover: "=", - onClick: '=', - onClickName: "=", - filterBy: "=", - itemMaxWidth: "@", - itemMaxHeight: "@", - itemMinWidth: "@", - itemMinHeight: "@", - onlyImages: "@" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbMediaGrid', MediaGridDirective); - -})(); - -(function () { - 'use strict'; - - function MiniListViewDirective(entityResource, iconHelper) { - - function link(scope, el, attr, ctrl) { - - scope.search = ""; - scope.miniListViews = []; - scope.breadcrumb = []; - - var miniListViewsHistory = []; - var goingForward = true; - var skipAnimation = true; - - function onInit() { - open(scope.node); - } - - function open(node) { - - // convert legacy icon for node - if(node && node.icon) { - node.icon = iconHelper.convertFromLegacyIcon(node.icon); - } - - goingForward = true; - - var miniListView = { - node: node, - loading: true, - pagination: { - pageSize: 10, - pageNumber: 1, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder", - orderBySystemField: true - } - }; - - // clear and push mini list view in dom so we only render 1 view + angular.module('umbraco.directives').directive('umbMediaGrid', MediaGridDirective); + }()); + (function () { + 'use strict'; + function MiniListViewDirective(entityResource, iconHelper) { + function link(scope, el, attr, ctrl) { + scope.search = ''; scope.miniListViews = []; - scope.miniListViews.push(miniListView); - - // store in history so we quickly can navigate back - miniListViewsHistory.push(miniListView); - - // get children - getChildrenForMiniListView(miniListView); - - makeBreadcrumb(); - - } - - function getChildrenForMiniListView(miniListView) { - - // start loading animation list view - miniListView.loading = true; - - entityResource.getPagedChildren(miniListView.node.id, scope.entityType, miniListView.pagination) - .then(function (data) { + scope.breadcrumb = []; + var miniListViewsHistory = []; + var goingForward = true; + var skipAnimation = true; + function onInit() { + open(scope.node); + } + function open(node) { + // convert legacy icon for node + if (node && node.icon) { + node.icon = iconHelper.convertFromLegacyIcon(node.icon); + } + goingForward = true; + var miniListView = { + node: node, + loading: true, + pagination: { + pageSize: 10, + pageNumber: 1, + filter: '', + orderDirection: 'Ascending', + orderBy: 'SortOrder', + orderBySystemField: true + } + }; + // clear and push mini list view in dom so we only render 1 view + scope.miniListViews = []; + scope.miniListViews.push(miniListView); + // store in history so we quickly can navigate back + miniListViewsHistory.push(miniListView); + // get children + getChildrenForMiniListView(miniListView); + makeBreadcrumb(); + } + function getChildrenForMiniListView(miniListView) { + // start loading animation list view + miniListView.loading = true; + entityResource.getPagedChildren(miniListView.node.id, scope.entityType, miniListView.pagination).then(function (data) { // update children miniListView.children = data.items; - _.each(miniListView.children, function(c) { + _.each(miniListView.children, function (c) { // convert legacy icon for node - if(c.icon) { + if (c.icon) { c.icon = iconHelper.convertFromLegacyIcon(c.icon); } // set published state for content if (c.metaData) { c.hasChildren = c.metaData.HasChildren; - if(scope.entityType === "Document") { + if (scope.entityType === 'Document') { c.published = c.metaData.IsPublished; } } @@ -9808,2403 +9087,2559 @@ Use this directive to generate a thumbnail grid of media items. // stop load indicator miniListView.loading = false; }); + } + scope.openNode = function (event, node) { + open(node); + event.stopPropagation(); + }; + scope.selectNode = function (node) { + if (scope.onSelect) { + scope.onSelect({ 'node': node }); + } + }; + /* Pagination */ + scope.goToPage = function (pageNumber, miniListView) { + // set new page number + miniListView.pagination.pageNumber = pageNumber; + // get children + getChildrenForMiniListView(miniListView); + }; + /* Breadcrumb */ + scope.clickBreadcrumb = function (ancestor) { + var found = false; + goingForward = false; + angular.forEach(miniListViewsHistory, function (historyItem, index) { + // We need to make sure we can compare the two id's. + // Some id's are integers and others are strings. + // Members have string ids like "all-members". + if (historyItem.node.id.toString() === ancestor.id.toString()) { + // load the list view from history + scope.miniListViews = []; + scope.miniListViews.push(historyItem); + // clean up history - remove all children after + miniListViewsHistory.splice(index + 1, miniListViewsHistory.length); + found = true; + } + }); + if (!found) { + // if we can't find the view in the history - close the list view + scope.exitMiniListView(); + } + // update the breadcrumb + makeBreadcrumb(); + }; + scope.showBackButton = function () { + // don't show the back button if the start node is a list view + if (scope.node.metaData && scope.node.metaData.IsContainer || scope.node.isContainer) { + return false; + } else { + return true; + } + }; + scope.exitMiniListView = function () { + miniListViewsHistory = []; + scope.miniListViews = []; + if (scope.onClose) { + scope.onClose(); + } + }; + function makeBreadcrumb() { + scope.breadcrumb = []; + angular.forEach(miniListViewsHistory, function (historyItem) { + scope.breadcrumb.push(historyItem.node); + }); + } + /* Search */ + scope.searchMiniListView = function (search, miniListView) { + // set search value + miniListView.pagination.filter = search; + // reset pagination + miniListView.pagination.pageNumber = 1; + // start loading animation list view + miniListView.loading = true; + searchMiniListView(miniListView); + }; + var searchMiniListView = _.debounce(function (miniListView) { + scope.$apply(function () { + getChildrenForMiniListView(miniListView); + }); + }, 500); + /* Animation */ + scope.getMiniListViewAnimation = function () { + // disable the first "slide-in-animation"" if the start node is a list view + if (scope.node.metaData && scope.node.metaData.IsContainer && skipAnimation || scope.node.isContainer && skipAnimation) { + skipAnimation = false; + return; + } + if (goingForward) { + return 'umb-mini-list-view--forward'; + } else { + return 'umb-mini-list-view--backwards'; + } + }; + onInit(); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-mini-list-view.html', + scope: { + node: '=', + entityType: '@', + startNodeId: '=', + onSelect: '&', + onClose: '&' + }, + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbMiniListView', MiniListViewDirective); + }()); + angular.module('umbraco.directives').directive('umbNestedContentEditor', [function () { + var link = function ($scope) { + // Clone the model because some property editors + // do weird things like updating and config values + // so we want to ensure we start from a fresh every + // time, we'll just sync the value back when we need to + $scope.model = angular.copy($scope.ngModel); + $scope.nodeContext = $scope.model; + // Find the selected tab + var selectedTab = $scope.model.tabs[0]; + if ($scope.tabAlias) { + angular.forEach($scope.model.tabs, function (tab) { + if (tab.alias.toLowerCase() === $scope.tabAlias.toLowerCase()) { + selectedTab = tab; + return; + } + }); + } + $scope.tab = selectedTab; + // Listen for sync request + var unsubscribe = $scope.$on('ncSyncVal', function (ev, args) { + if (args.key === $scope.model.key) { + // Tell inner controls we are submitting + $scope.$broadcast('formSubmitting', { scope: $scope }); + // Sync the values back + angular.forEach($scope.ngModel.tabs, function (tab) { + if (tab.alias.toLowerCase() === selectedTab.alias.toLowerCase()) { + var localPropsMap = selectedTab.properties.reduce(function (map, obj) { + map[obj.alias] = obj; + return map; + }, {}); + angular.forEach(tab.properties, function (prop) { + if (localPropsMap.hasOwnProperty(prop.alias)) { + prop.value = localPropsMap[prop.alias].value; + } + }); + } + }); + } + }); + $scope.$on('$destroy', function () { + unsubscribe(); + }); + }; + return { + restrict: 'E', + replace: true, + templateUrl: Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/views/propertyeditors/nestedcontent/nestedcontent.editor.html', + scope: { + ngModel: '=', + tabAlias: '=' + }, + link: link + }; + }]); + //angular.module("umbraco.directives").directive('nestedContentSubmitWatcher', function () { + // var link = function (scope) { + // // call the load callback on scope to obtain the ID of this submit watcher + // var id = scope.loadCallback(); + // scope.$on("formSubmitting", function (ev, args) { + // // on the "formSubmitting" event, call the submit callback on scope to notify the nestedContent controller to do it's magic + // if (id === scope.activeSubmitWatcher) { + // scope.submitCallback(); + // } + // }); + // } + // return { + // restrict: "E", + // replace: true, + // template: "", + // scope: { + // loadCallback: '=', + // submitCallback: '=', + // activeSubmitWatcher: '=' + // }, + // link: link + // } + //}); + /** +@ngdoc directive +@name umbraco.directives.directive:umbNodePreview +@restrict E +@scope + +@description +Added in Umbraco v. 7.6: Use this directive to render a node preview. + +

    Markup example

    +
    +    
    + +
    + + +
    + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +    
    +        function Controller() {
    +    
    +            var vm = this;
    +    
    +            vm.allowRemove = true;
    +            vm.allowOpen = true;
    +            vm.sortable = true;
    +    
    +            vm.nodes = [
    +                {
    +                    "icon": "icon-document",
    +                    "name": "My node 1",
    +                    "published": true,
    +                    "description": "A short description of my node"
    +                },
    +                {
    +                    "icon": "icon-document",
    +                    "name": "My node 2",
    +                    "published": true,
    +                    "description": "A short description of my node"
    +                }
    +            ];
    +    
    +            vm.remove = remove;
    +            vm.open = open;
    +    
    +            function remove(index, nodes) {
    +                alert("remove node");
    +            }
    +    
    +            function open(node) {
    +                alert("open node");
    +            }
    +    
    +        }
    +    
    +        angular.module("umbraco").controller("My.NodePreviewController", Controller);
    +    
    +    })();
    +
    + +@param {string} icon (binding): The node icon. +@param {string} name (binding): The node name. +@param {boolean} published (binding): The node pusblished state. +@param {string} description (binding): A short description. +@param {boolean} sortable (binding): Will add a move cursor on the node preview. Can used in combination with ui-sortable. +@param {boolean} allowRemove (binding): Show/Hide the remove button. +@param {boolean} allowOpen (binding): Show/Hide the open button. +@param {boolean} allowEdit (binding): Show/Hide the edit button (Added in version 7.7.0). +@param {function} onRemove (expression): Callback function when the remove button is clicked. +@param {function} onOpen (expression): Callback function when the open button is clicked. +@param {function} onEdit (expression): Callback function when the edit button is clicked (Added in version 7.7.0). +**/ + (function () { + 'use strict'; + function NodePreviewDirective() { + function link(scope, el, attr, ctrl) { + if (!scope.editLabelKey) { + scope.editLabelKey = 'general_edit'; + } } - - scope.openNode = function(event, node) { - open(node); - event.stopPropagation(); + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-node-preview.html', + scope: { + icon: '=?', + name: '=', + description: '=?', + permissions: '=?', + published: '=?', + sortable: '=?', + allowOpen: '=?', + allowRemove: '=?', + allowEdit: '=?', + onOpen: '&?', + onRemove: '&?', + onEdit: '&?' + }, + link: link }; - - scope.selectNode = function(node) { - if(scope.onSelect) { - scope.onSelect({'node': node}); + return directive; + } + angular.module('umbraco.directives').directive('umbNodePreview', NodePreviewDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbPagination +@restrict E +@scope + +@description +Use this directive to generate a pagination. + +

    Markup example

    +
    +    
    + + + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +
    +            vm.pagination = {
    +                pageNumber: 1,
    +                totalPages: 10
    +            }
    +
    +            vm.nextPage = nextPage;
    +            vm.prevPage = prevPage;
    +            vm.goToPage = goToPage;
    +
    +            function nextPage(pageNumber) {
    +                // do magic here
    +                console.log(pageNumber);
    +                alert("nextpage");
    +            }
    +
    +            function prevPage(pageNumber) {
    +                // do magic here
    +                console.log(pageNumber);
    +                alert("prevpage");
    +            }
    +
    +            function goToPage(pageNumber) {
    +                // do magic here
    +                console.log(pageNumber);
    +                alert("go to");
    +            }
    +
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +    })();
    +
    + +@param {number} pageNumber (binding): Current page number. +@param {number} totalPages (binding): The total number of pages. +@param {callback} onNext (binding): Callback method to go to the next page. +

    The callback returns:

    +
      +
    • pageNumber: The page number
    • +
    +@param {callback=} onPrev (binding): Callback method to go to the previous page. +

    The callback returns:

    +
      +
    • pageNumber: The page number
    • +
    +@param {callback=} onGoToPage (binding): Callback method to go to a specific page. +

    The callback returns:

    +
      +
    • pageNumber: The page number
    • +
    +**/ + (function () { + 'use strict'; + function PaginationDirective(localizationService) { + function link(scope, el, attr, ctrl) { + function activate() { + scope.pagination = []; + var i = 0; + if (scope.totalPages <= 10) { + for (i = 0; i < scope.totalPages; i++) { + scope.pagination.push({ + val: i + 1, + isActive: scope.pageNumber === i + 1 + }); + } + } else { + //if there is more than 10 pages, we need to do some fancy bits + //get the max index to start + var maxIndex = scope.totalPages - 10; + //set the start, but it can't be below zero + var start = Math.max(scope.pageNumber - 5, 0); + //ensure that it's not too far either + start = Math.min(maxIndex, start); + for (i = start; i < 10 + start; i++) { + scope.pagination.push({ + val: i + 1, + isActive: scope.pageNumber === i + 1 + }); + } + //now, if the start is greater than 0 then '1' will not be displayed, so do the elipses thing + if (start > 0) { + scope.pagination.unshift({ + name: localizationService.localize('general_first'), + val: 1, + isActive: false + }, { + val: '...', + isActive: false + }); + } + //same for the end + if (start < maxIndex) { + scope.pagination.push({ + val: '...', + isActive: false + }, { + name: localizationService.localize('general_last'), + val: scope.totalPages, + isActive: false + }); + } + } } - }; - - /* Pagination */ - scope.goToPage = function(pageNumber, miniListView) { - // set new page number - miniListView.pagination.pageNumber = pageNumber; - // get children - getChildrenForMiniListView(miniListView); - }; - - /* Breadcrumb */ - scope.clickBreadcrumb = function(ancestor) { - - var found = false; - goingForward = false; - - angular.forEach(miniListViewsHistory, function(historyItem, index){ - // We need to make sure we can compare the two id's. - // Some id's are integers and others are strings. - // Members have string ids like "all-members". - if(historyItem.node.id.toString() === ancestor.id.toString()) { - // load the list view from history - scope.miniListViews = []; - scope.miniListViews.push(historyItem); - // clean up history - remove all children after - miniListViewsHistory.splice(index + 1, miniListViewsHistory.length); - found = true; + scope.next = function () { + if (scope.pageNumber < scope.totalPages) { + scope.pageNumber++; + if (scope.onNext) { + scope.onNext(scope.pageNumber); + } + if (scope.onChange) { + scope.onChange({ 'pageNumber': scope.pageNumber }); + } + } + }; + scope.prev = function (pageNumber) { + if (scope.pageNumber > 1) { + scope.pageNumber--; + if (scope.onPrev) { + scope.onPrev(scope.pageNumber); + } + if (scope.onChange) { + scope.onChange({ 'pageNumber': scope.pageNumber }); + } + } + }; + scope.goToPage = function (pageNumber) { + scope.pageNumber = pageNumber + 1; + if (scope.onGoToPage) { + scope.onGoToPage(scope.pageNumber); + } + if (scope.onChange) { + if (scope.onChange) { + scope.onChange({ 'pageNumber': scope.pageNumber }); + } } + }; + var unbindPageNumberWatcher = scope.$watch('pageNumber', function (newValue, oldValue) { + activate(); + }); + scope.$on('$destroy', function () { + unbindPageNumberWatcher(); }); - - if(!found) { - // if we can't find the view in the history - close the list view - scope.exitMiniListView(); - } - - // update the breadcrumb - makeBreadcrumb(); - + activate(); + } + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-pagination.html', + scope: { + pageNumber: '=', + totalPages: '=', + onNext: '=', + onPrev: '=', + onGoToPage: '=', + onChange: '&' + }, + link: link }; - - scope.showBackButton = function() { - // don't show the back button if the start node is a list view - if(scope.node.metaData && scope.node.metaData.IsContainer || scope.node.isContainer) { - return false; - } else { - return true; + return directive; + } + angular.module('umbraco.directives').directive('umbPagination', PaginationDirective); + }()); + (function () { + 'use strict'; + // comes from https://codepen.io/jakob-e/pen/eNBQaP + // works fine with Angular 1.6.5 - alas not with 1.1.5 - binding issue + function PasswordToggleDirective($compile) { + var directive = { + restrict: 'A', + scope: {}, + link: function (scope, elem, attrs) { + scope.tgl = function () { + elem.attr('type', elem.attr('type') === 'text' ? 'password' : 'text'); + }; + var lnk = angular.element('Toggle'); + $compile(lnk)(scope); + elem.wrap('
    ').after(lnk); } }; - - scope.exitMiniListView = function() { - miniListViewsHistory = []; - scope.miniListViews = []; - if(scope.onClose) { - scope.onClose(); + return directive; + } + angular.module('umbraco.directives').directive('umbPasswordToggle', PasswordToggleDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbProgressBar +@restrict E +@scope + +@description +Use this directive to generate a progress bar. + +

    Markup example

    +
    +    
    +    
    +
    + +@param {number} percentage (attribute): The progress in percentage. +@param {string} size (attribute): The size (s, m). + +**/ + (function () { + 'use strict'; + function ProgressBarDirective() { + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-progress-bar.html', + scope: { + percentage: '@', + size: '@?' } }; - - function makeBreadcrumb() { - scope.breadcrumb = []; - angular.forEach(miniListViewsHistory, function(historyItem){ - scope.breadcrumb.push(historyItem.node); - }); - } - - /* Search */ - scope.searchMiniListView = function(search, miniListView) { - // set search value - miniListView.pagination.filter = search; - // reset pagination - miniListView.pagination.pageNumber = 1; - // start loading animation list view - miniListView.loading = true; - searchMiniListView(miniListView); - }; - - var searchMiniListView = _.debounce(function (miniListView) { - scope.$apply(function () { - getChildrenForMiniListView(miniListView); - }); - }, 500); - - /* Animation */ - scope.getMiniListViewAnimation = function() { - - // disable the first "slide-in-animation"" if the start node is a list view - if(scope.node.metaData && scope.node.metaData.IsContainer && skipAnimation || scope.node.isContainer && skipAnimation) { - skipAnimation = false; - return; + return directive; + } + angular.module('umbraco.directives').directive('umbProgressBar', ProgressBarDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbStickyBar +@restrict A + +@description +Use this directive make an element sticky and follow the page when scrolling. + +

    Markup example

    +
    +    
    + +
    +
    + +
    +
    + +

    CSS example

    +
    +    .my-sticky-bar {
    +        padding: 15px 0;
    +        background: #000000;
    +        position: relative;
    +        top: 0;
    +    }
    +
    +    .my-sticky-bar.-umb-sticky-bar {
    +        top: 100px;
    +    }
    +
    + +@param {string} scrollableContainer Set the class (".element") or the id ("#element") of the scrollable container element. +**/ + (function () { + 'use strict'; + function StickyBarDirective($rootScope) { + function link(scope, el, attr, ctrl) { + var bar = $(el); + var scrollableContainer = null; + var clonedBar = null; + var cloneIsMade = false; + function activate() { + if (attr.scrollableContainer) { + scrollableContainer = $(attr.scrollableContainer); + } else { + scrollableContainer = $(window); + } + scrollableContainer.on('scroll.umbStickyBar', determineVisibility).trigger('scroll'); + $(window).on('resize.umbStickyBar', determineVisibility); + scope.$on('$destroy', function () { + scrollableContainer.off('.umbStickyBar'); + $(window).off('.umbStickyBar'); + }); } - - if(goingForward) { - return 'umb-mini-list-view--forward'; - } else { - return 'umb-mini-list-view--backwards'; + function determineVisibility() { + var barTop = bar[0].offsetTop; + var scrollTop = scrollableContainer.scrollTop(); + if (scrollTop > barTop) { + if (!cloneIsMade) { + createClone(); + clonedBar.css({ 'visibility': 'visible' }); + } else { + calculateSize(); + } + } else { + if (cloneIsMade) { + //remove cloned element (switched places with original on creation) + bar.remove(); + bar = clonedBar; + clonedBar = null; + bar.removeClass('-umb-sticky-bar'); + bar.css({ + position: 'relative', + 'width': 'auto', + 'height': 'auto', + 'z-index': 'auto', + 'visibility': 'visible' + }); + cloneIsMade = false; + } + } + } + function calculateSize() { + clonedBar.css({ + width: bar.outerWidth(), + height: bar.height() + }); + } + function createClone() { + //switch place with cloned element, to keep binding intact + clonedBar = bar; + bar = clonedBar.clone(); + clonedBar.after(bar); + clonedBar.addClass('-umb-sticky-bar'); + clonedBar.css({ + 'position': 'fixed', + 'z-index': 500, + 'visibility': 'hidden' + }); + cloneIsMade = true; + calculateSize(); } + activate(); + } + var directive = { + restrict: 'A', + link: link }; - - onInit(); - + return directive; } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-mini-list-view.html', - scope: { - node: "=", - entityType: "@", - startNodeId: "=", - onSelect: "&", - onClose: "&" - }, - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbMiniListView', MiniListViewDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbNodePreview -@restrict E -@scope - -@description -Added in Umbraco v. 7.6: Use this directive to render a node preview. - -

    Markup example

    -
    -    
    - -
    - - -
    - -
    -
    - -

    Controller example

    -
    +        angular.module('umbraco.directives').directive('umbStickyBar', StickyBarDirective);
    +    }());
         (function () {
    -        "use strict";
    -    
    -        function Controller() {
    -    
    -            var vm = this;
    -    
    -            vm.allowRemove = true;
    -            vm.allowOpen = true;
    -            vm.sortable = true;
    -    
    -            vm.nodes = [
    -                {
    -                    "icon": "icon-document",
    -                    "name": "My node 1",
    -                    "published": true,
    -                    "description": "A short description of my node"
    +        'use strict';
    +        function TableDirective(iconHelper) {
    +            function link(scope, el, attr, ctrl) {
    +                scope.clickItem = function (item, $event) {
    +                    if (scope.onClick) {
    +                        scope.onClick(item);
    +                        $event.stopPropagation();
    +                    }
    +                };
    +                scope.selectItem = function (item, $index, $event) {
    +                    if (scope.onSelect) {
    +                        scope.onSelect(item, $index, $event);
    +                        $event.stopPropagation();
    +                    }
    +                };
    +                scope.selectAll = function ($event) {
    +                    if (scope.onSelectAll) {
    +                        scope.onSelectAll($event);
    +                    }
    +                };
    +                scope.isSelectedAll = function () {
    +                    if (scope.onSelectedAll && scope.items && scope.items.length > 0) {
    +                        return scope.onSelectedAll();
    +                    }
    +                };
    +                scope.isSortDirection = function (col, direction) {
    +                    if (scope.onSortingDirection) {
    +                        return scope.onSortingDirection(col, direction);
    +                    }
    +                };
    +                scope.sort = function (field, allow, isSystem) {
    +                    if (scope.onSort) {
    +                        scope.onSort(field, allow, isSystem);
    +                    }
    +                };
    +                scope.getIcon = function (entry) {
    +                    return iconHelper.convertFromLegacyIcon(entry.icon);
    +                };
    +            }
    +            var directive = {
    +                restrict: 'E',
    +                replace: true,
    +                templateUrl: 'views/components/umb-table.html',
    +                scope: {
    +                    items: '=',
    +                    itemProperties: '=',
    +                    allowSelectAll: '=',
    +                    onSelect: '=',
    +                    onClick: '=',
    +                    onSelectAll: '=',
    +                    onSelectedAll: '=',
    +                    onSortingDirection: '=',
    +                    onSort: '='
                     },
    -                {
    -                    "icon": "icon-document",
    -                    "name": "My node 2",
    -                    "published": true,
    -                    "description": "A short description of my node"
    +                link: link
    +            };
    +            return directive;
    +        }
    +        angular.module('umbraco.directives').directive('umbTable', TableDirective);
    +    }());
    +    /**
    +@ngdoc directive
    +@name umbraco.directives.directive:umbTooltip
    +@restrict E
    +@scope
    +
    +@description
    +Use this directive to render a tooltip.
    +
    +

    Markup example

    +
    +    
    + +
    + Hover me +
    + + + // tooltip content here + + +
    +
    + +

    Controller example

    +
    +    (function () {
    +        "use strict";
    +
    +        function Controller() {
    +
    +            var vm = this;
    +            vm.tooltip = {
    +                show: false,
    +                event: null
    +            };
    +
    +            vm.mouseOver = mouseOver;
    +            vm.mouseLeave = mouseLeave;
    +
    +            function mouseOver($event) {
    +                vm.tooltip = {
    +                    show: true,
    +                    event: $event
    +                };
    +            }
    +
    +            function mouseLeave() {
    +                vm.tooltip = {
    +                    show: false,
    +                    event: null
    +                };
    +            }
    +
    +        }
    +
    +        angular.module("umbraco").controller("My.Controller", Controller);
    +
    +    })();
    +
    + +@param {string} event Set the $event from the target element to position the tooltip relative to the mouse cursor. +**/ + (function () { + 'use strict'; + function TooltipDirective($timeout) { + function link(scope, el, attr, ctrl) { + scope.tooltipStyles = {}; + scope.tooltipStyles.left = 0; + scope.tooltipStyles.top = 0; + function activate() { + $timeout(function () { + setTooltipPosition(scope.event); + }); } - ]; - - vm.remove = remove; - vm.open = open; - - function remove(index, nodes) { - alert("remove node"); - } - - function open(node) { - alert("open node"); + function setTooltipPosition(event) { + var container = $('#contentwrapper'); + var containerLeft = container[0].offsetLeft; + var containerRight = containerLeft + container[0].offsetWidth; + var containerTop = container[0].offsetTop; + var containerBottom = containerTop + container[0].offsetHeight; + var elementHeight = null; + var elementWidth = null; + var position = { + right: 'inherit', + left: 'inherit', + top: 'inherit', + bottom: 'inherit' + }; + // element size + elementHeight = el.context.clientHeight; + elementWidth = el.context.clientWidth; + position.left = event.pageX - elementWidth / 2; + position.top = event.pageY; + // check to see if element is outside screen + // outside right + if (position.left + elementWidth > containerRight) { + position.right = 10; + position.left = 'inherit'; + } + // outside bottom + if (position.top + elementHeight > containerBottom) { + position.bottom = 10; + position.top = 'inherit'; + } + // outside left + if (position.left < containerLeft) { + position.left = containerLeft + 10; + position.right = 'inherit'; + } + // outside top + if (position.top < containerTop) { + position.top = 10; + position.bottom = 'inherit'; + } + scope.tooltipStyles = position; + el.css(position); + } + activate(); } - + var directive = { + restrict: 'E', + transclude: true, + replace: true, + templateUrl: 'views/components/umb-tooltip.html', + scope: { event: '=' }, + link: link + }; + return directive; } - - angular.module("umbraco").controller("My.NodePreviewController", Controller); - - })(); -
    - -@param {string} icon (binding): The node icon. -@param {string} name (binding): The node name. -@param {boolean} published (binding): The node pusblished state. -@param {string} description (binding): A short description. -@param {boolean} sortable (binding): Will add a move cursor on the node preview. Can used in combination with ui-sortable. -@param {boolean} allowRemove (binding): Show/Hide the remove button. -@param {boolean} allowOpen (binding): Show/Hide the open button. -@param {function} onRemove (expression): Callback function when the remove button is clicked. -@param {function} onOpen (expression): Callback function when the open button is clicked. + angular.module('umbraco.directives').directive('umbTooltip', TooltipDirective); + }()); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbFileDropzone +* @restrict E +* @function +* @description +* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. **/ - -(function () { - 'use strict'; - - function NodePreviewDirective() { - - function link(scope, el, attr, ctrl) { - - } - - var directive = { + /* +TODO +.directive("umbFileDrop", function ($timeout, $upload, localizationService, umbRequestHelper){ + return{ + restrict: "A", + link: function(scope, element, attrs){ + //load in the options model + } + } +}) +*/ + angular.module('umbraco.directives').directive('umbFileDropzone', function ($timeout, Upload, localizationService, umbRequestHelper) { + return { restrict: 'E', replace: true, - templateUrl: 'views/components/umb-node-preview.html', + templateUrl: 'views/components/upload/umb-file-dropzone.html', scope: { - icon: "=?", - name: "=", - description: "=?", - published: "=?", - sortable: "=?", - allowOpen: "=?", - allowRemove: "=?", - onOpen: "&?", - onRemove: "&?" + parentId: '@', + contentTypeAlias: '@', + propertyAlias: '@', + accept: '@', + maxFileSize: '@', + compact: '@', + hideDropzone: '@', + acceptedMediatypes: '=', + filesQueued: '=', + handleFile: '=', + filesUploaded: '=' }, - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbNodePreview', NodePreviewDirective); - -})(); -/** -@ngdoc directive -@name umbraco.directives.directive:umbPagination -@restrict E -@scope - -@description -Use this directive to generate a pagination. - -

    Markup example

    -
    -    
    - - - - -
    -
    - -

    Controller example

    -
    -    (function () {
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -
    -            vm.pagination = {
    -                pageNumber: 1,
    -                totalPages: 10
    -            }
    -
    -            vm.nextPage = nextPage;
    -            vm.prevPage = prevPage;
    -            vm.goToPage = goToPage;
    -
    -            function nextPage(pageNumber) {
    -                // do magic here
    -                console.log(pageNumber);
    -                alert("nextpage");
    -            }
    -
    -            function prevPage(pageNumber) {
    -                // do magic here
    -                console.log(pageNumber);
    -                alert("prevpage");
    -            }
    -
    -            function goToPage(pageNumber) {
    -                // do magic here
    -                console.log(pageNumber);
    -                alert("go to");
    -            }
    -
    -        }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -    })();
    -
    - -@param {number} pageNumber (binding): Current page number. -@param {number} totalPages (binding): The total number of pages. -@param {callback} onNext (binding): Callback method to go to the next page. -

    The callback returns:

    -
      -
    • pageNumber: The page number
    • -
    -@param {callback=} onPrev (binding): Callback method to go to the previous page. -

    The callback returns:

    -
      -
    • pageNumber: The page number
    • -
    -@param {callback=} onGoToPage (binding): Callback method to go to a specific page. -

    The callback returns:

    -
      -
    • pageNumber: The page number
    • -
    -**/ - -(function() { - 'use strict'; - - function PaginationDirective() { - - function link(scope, el, attr, ctrl) { - - function activate() { - - scope.pagination = []; - - var i = 0; - - if (scope.totalPages <= 10) { - for (i = 0; i < scope.totalPages; i++) { - scope.pagination.push({ - val: (i + 1), - isActive: scope.pageNumber === (i + 1) - }); + link: function (scope, element, attrs) { + scope.queue = []; + scope.done = []; + scope.rejected = []; + scope.currentFile = undefined; + function _filterFile(file) { + var ignoreFileNames = ['Thumbs.db']; + var ignoreFileTypes = ['directory']; + // ignore files with names from the list + // ignore files with types from the list + // ignore files which starts with "." + if (ignoreFileNames.indexOf(file.name) === -1 && ignoreFileTypes.indexOf(file.type) === -1 && file.name.indexOf('.') !== 0) { + return true; + } else { + return false; + } } - } - else { - //if there is more than 10 pages, we need to do some fancy bits - - //get the max index to start - var maxIndex = scope.totalPages - 10; - //set the start, but it can't be below zero - var start = Math.max(scope.pageNumber - 5, 0); - //ensure that it's not too far either - start = Math.min(maxIndex, start); - - for (i = start; i < (10 + start) ; i++) { - scope.pagination.push({ - val: (i + 1), - isActive: scope.pageNumber === (i + 1) + function _filesQueued(files, event) { + //Push into the queue + angular.forEach(files, function (file) { + if (_filterFile(file) === true) { + if (file.$error) { + scope.rejected.push(file); + } else { + scope.queue.push(file); + } + } }); + //when queue is done, kick the uploader + if (!scope.working) { + // Upload not allowed + if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) { + files.map(function (file) { + file.uploadStatus = 'error'; + file.serverErrorMessage = 'File type is not allowed here'; + scope.rejected.push(file); + }); + scope.queue = []; + } + // One allowed type + if (scope.acceptedMediatypes && scope.acceptedMediatypes.length === 1) { + // Standard setup - set alias to auto select to let the server best decide which media type to use + if (scope.acceptedMediatypes[0].alias === 'Image') { + scope.contentTypeAlias = 'umbracoAutoSelect'; + } else { + scope.contentTypeAlias = scope.acceptedMediatypes[0].alias; + } + _processQueueItem(); + } + // More than one, open dialog + if (scope.acceptedMediatypes && scope.acceptedMediatypes.length > 1) { + _chooseMediaType(); + } + } + } + function _processQueueItem() { + if (scope.queue.length > 0) { + scope.currentFile = scope.queue.shift(); + _upload(scope.currentFile); + } else if (scope.done.length > 0) { + if (scope.filesUploaded) { + //queue is empty, trigger the done action + scope.filesUploaded(scope.done); + } + //auto-clear the done queue after 3 secs + var currentLength = scope.done.length; + $timeout(function () { + scope.done.splice(0, currentLength); + }, 3000); + } } - - //now, if the start is greater than 0 then '1' will not be displayed, so do the elipses thing - if (start > 0) { - scope.pagination.unshift({ name: "First", val: 1, isActive: false }, {val: "...",isActive: false}); + function _upload(file) { + scope.propertyAlias = scope.propertyAlias ? scope.propertyAlias : 'umbracoFile'; + scope.contentTypeAlias = scope.contentTypeAlias ? scope.contentTypeAlias : 'Image'; + Upload.upload({ + url: umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'PostAddFile'), + fields: { + 'currentFolder': scope.parentId, + 'contentTypeAlias': scope.contentTypeAlias, + 'propertyAlias': scope.propertyAlias, + 'path': file.path + }, + file: file + }).progress(function (evt) { + if (file.uploadStat !== 'done' && file.uploadStat !== 'error') { + // calculate progress in percentage + var progressPercentage = parseInt(100 * evt.loaded / evt.total, 10); + // set percentage property on file + file.uploadProgress = progressPercentage; + // set uploading status on file + file.uploadStatus = 'uploading'; + } + }).success(function (data, status, headers, config) { + if (data.notifications && data.notifications.length > 0) { + // set error status on file + file.uploadStatus = 'error'; + // Throw message back to user with the cause of the error + file.serverErrorMessage = data.notifications[0].message; + // Put the file in the rejected pool + scope.rejected.push(file); + } else { + // set done status on file + file.uploadStatus = 'done'; + file.uploadProgress = 100; + // set date/time for when done - used for sorting + file.doneDate = new Date(); + // Put the file in the done pool + scope.done.push(file); + } + scope.currentFile = undefined; + //after processing, test if everthing is done + _processQueueItem(); + }).error(function (evt, status, headers, config) { + // set status done + file.uploadStatus = 'error'; + //if the service returns a detailed error + if (evt.InnerException) { + file.serverErrorMessage = evt.InnerException.ExceptionMessage; + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf('ValidateRequestEntityLength') > 0) { + file.serverErrorMessage = 'File too large to upload'; + } + } else if (evt.Message) { + file.serverErrorMessage = evt.Message; + } + // If file not found, server will return a 404 and display this message + if (status === 404) { + file.serverErrorMessage = 'File not found'; + } + //after processing, test if everthing is done + scope.rejected.push(file); + scope.currentFile = undefined; + _processQueueItem(); + }); } - - //same for the end - if (start < maxIndex) { - scope.pagination.push({ val: "...", isActive: false }, { name: "Last", val: scope.totalPages, isActive: false }); + function _chooseMediaType() { + scope.mediatypepickerOverlay = { + view: 'mediatypepicker', + title: 'Choose media type', + acceptedMediatypes: scope.acceptedMediatypes, + hideSubmitButton: true, + show: true, + submit: function (model) { + scope.contentTypeAlias = model.selectedType.alias; + scope.mediatypepickerOverlay.show = false; + scope.mediatypepickerOverlay = null; + _processQueueItem(); + }, + close: function (oldModel) { + scope.queue.map(function (file) { + file.uploadStatus = 'error'; + file.serverErrorMessage = 'Cannot upload this file, no mediatype selected'; + scope.rejected.push(file); + }); + scope.queue = []; + scope.mediatypepickerOverlay.show = false; + scope.mediatypepickerOverlay = null; + } + }; } + scope.handleFiles = function (files, event) { + if (scope.filesQueued) { + scope.filesQueued(files, event); + } + _filesQueued(files, event); + }; } - - } - - scope.next = function () { - if (scope.pageNumber < scope.totalPages) { - scope.pageNumber++; - if (scope.onNext) { - scope.onNext(scope.pageNumber); - } - if (scope.onChange) { - scope.onChange({ "pageNumber": scope.pageNumber }); - } - } - }; - - scope.prev = function (pageNumber) { - if (scope.pageNumber > 1) { - scope.pageNumber--; - if (scope.onPrev) { - scope.onPrev(scope.pageNumber); - } - if (scope.onChange) { - scope.onChange({ "pageNumber": scope.pageNumber }); - } - } - }; - - scope.goToPage = function (pageNumber) { - scope.pageNumber = pageNumber + 1; - if (scope.onGoToPage) { - scope.onGoToPage(scope.pageNumber); - } - if (scope.onChange) { - if (scope.onChange) { - scope.onChange({ "pageNumber": scope.pageNumber }); - } - } - }; - - var unbindPageNumberWatcher = scope.$watch('pageNumber', function(newValue, oldValue){ - activate(); - }); - - scope.$on('$destroy', function(){ - unbindPageNumberWatcher(); - }); - - activate(); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-pagination.html', - scope: { - pageNumber: "=", - totalPages: "=", - onNext: "=", - onPrev: "=", - onGoToPage: "=", - onChange: "&" - }, - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbPagination', PaginationDirective); - -})(); - - -/** -@ngdoc directive -@name umbraco.directives.directive:umbProgressBar -@restrict E -@scope - -@description -Use this directive to generate a progress bar. - -

    Markup example

    -
    -    
    -    
    -
    - -@param {number} percentage (attribute): The progress in percentage. + }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbFileUpload +* @function +* @restrict A +* @scope +* @description +* Listens for file input control changes and emits events when files are selected for use in other controllers. **/ - -(function() { - 'use strict'; - - function ProgressBarDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-progress-bar.html', - scope: { - percentage: "@" + function umbFileUpload() { + return { + restrict: 'A', + scope: true, + //create a new scope + link: function (scope, el, attrs) { + el.bind('change', function (event) { + var files = event.target.files; + //emit event upward + scope.$emit('filesSelected', { files: files }); + }); } }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbProgressBar', ProgressBarDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbStickyBar -@restrict A - -@description -Use this directive make an element sticky and follow the page when scrolling. - -

    Markup example

    -
    -    
    - -
    -
    - -
    -
    - -

    CSS example

    -
    -    .my-sticky-bar {
    -        padding: 15px 0;
    -        background: #000000;
    -        position: relative;
    -        top: 0;
         }
    -
    -    .my-sticky-bar.-umb-sticky-bar {
    -        top: 100px;
    -    }
    -
    - -@param {string} scrollableContainer Set the class (".element") or the id ("#element") of the scrollable container element. + angular.module('umbraco.directives').directive('umbFileUpload', umbFileUpload); + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbSingleFileUpload +* @function +* @restrict A +* @scope +* @description +* A single file upload field that will reset itself based on the object passed in for the rebuild parameter. This +* is required because the only way to reset an upload control is to replace it's html. **/ - -(function () { - 'use strict'; - - function StickyBarDirective($rootScope) { - - function link(scope, el, attr, ctrl) { - - var bar = $(el); - var scrollableContainer = null; - var clonedBar = null; - var cloneIsMade = false; - - function activate() { - - if (attr.scrollableContainer) { - scrollableContainer = $(attr.scrollableContainer); - } else { - scrollableContainer = $(window); - } - - scrollableContainer.on('scroll.umbStickyBar', determineVisibility).trigger("scroll"); - $(window).on('resize.umbStickyBar', determineVisibility); - - scope.$on('$destroy', function () { - scrollableContainer.off('.umbStickyBar'); - $(window).off('.umbStickyBar'); - }); - - } - - function determineVisibility() { - - var barTop = bar[0].offsetTop; - var scrollTop = scrollableContainer.scrollTop(); - - if (scrollTop > barTop) { - - if (!cloneIsMade) { - - createClone(); - - clonedBar.css({ - 'visibility': 'visible' - }); - - } else { - - calculateSize(); - - } - - } else { - - if (cloneIsMade) { - - //remove cloned element (switched places with original on creation) - bar.remove(); - bar = clonedBar; - clonedBar = null; - - bar.removeClass('-umb-sticky-bar'); - bar.css({ - position: 'relative', - 'width': 'auto', - 'height': 'auto', - 'z-index': 'auto', - 'visibility': 'visible' - }); - - cloneIsMade = false; - + function umbSingleFileUpload($compile) { + return { + restrict: 'E', + scope: { rebuild: '=' }, + replace: true, + template: '
    ', + link: function (scope, el, attrs) { + scope.$watch('rebuild', function (newVal, oldVal) { + if (newVal && newVal !== oldVal) { + //recompile it! + el.html(''); + $compile(el.contents())(scope); } - - } - - } - - function calculateSize() { - clonedBar.css({ - width: bar.outerWidth(), - height: bar.height() - }); - } - - function createClone() { - //switch place with cloned element, to keep binding intact - clonedBar = bar; - bar = clonedBar.clone(); - clonedBar.after(bar); - clonedBar.addClass('-umb-sticky-bar'); - clonedBar.css({ - 'position': 'fixed', - 'z-index': 500, - 'visibility': 'hidden' }); - - cloneIsMade = true; - calculateSize(); - } - - activate(); - - } - - var directive = { - restrict: 'A', - link: link }; - - return directive; } - - angular.module('umbraco.directives').directive('umbStickyBar', StickyBarDirective); - -})(); - -(function () { - 'use strict'; - - function TableDirective(iconHelper) { - - function link(scope, el, attr, ctrl) { - - scope.clickItem = function (item, $event) { - if (scope.onClick) { - scope.onClick(item); - $event.stopPropagation(); - } - }; - - scope.selectItem = function (item, $index, $event) { - if (scope.onSelect) { - scope.onSelect(item, $index, $event); - $event.stopPropagation(); - } - }; - - scope.selectAll = function ($event) { - if (scope.onSelectAll) { - scope.onSelectAll($event); - } - }; - - scope.isSelectedAll = function () { - if (scope.onSelectedAll && scope.items && scope.items.length > 0) { - return scope.onSelectedAll(); - } - }; - - scope.isSortDirection = function (col, direction) { - if (scope.onSortingDirection) { - return scope.onSortingDirection(col, direction); - } - }; - - scope.sort = function (field, allow, isSystem) { - if (scope.onSort) { - scope.onSort(field, allow, isSystem); - } - }; - - scope.getIcon = function (entry) { - return iconHelper.convertFromLegacyIcon(entry.icon); - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-table.html', - scope: { - items: '=', - itemProperties: '=', - allowSelectAll: '=', - onSelect: '=', - onClick: '=', - onSelectAll: '=', - onSelectedAll: '=', - onSortingDirection: '=', - onSort: '=' - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbTable', TableDirective); - -})(); - -/** -@ngdoc directive -@name umbraco.directives.directive:umbTooltip -@restrict E -@scope - -@description -Use this directive to render a tooltip. - -

    Markup example

    -
    -    
    - -
    - Hover me -
    - - - // tooltip content here - - -
    -
    - -

    Controller example

    -
    +    angular.module('umbraco.directives').directive('umbSingleFileUpload', umbSingleFileUpload);
         (function () {
    -        "use strict";
    -
    -        function Controller() {
    -
    -            var vm = this;
    -            vm.tooltip = {
    -                show: false,
    -                event: null
    +        'use strict';
    +        function ChangePasswordController($scope) {
    +            function resetModel(isNew) {
    +                //the model config will contain an object, if it does not we'll create defaults
    +                //NOTE: We will not support doing the password regex on the client side because the regex on the server side
    +                //based on the membership provider cannot always be ported to js from .net directly.        
    +                /*
    +      {
    +          hasPassword: true/false,
    +          requiresQuestionAnswer: true/false,
    +          enableReset: true/false,
    +          enablePasswordRetrieval: true/false,
    +          minPasswordLength: 10
    +      }
    +      */
    +                $scope.showReset = false;
    +                //set defaults if they are not available
    +                if ($scope.config.disableToggle === undefined) {
    +                    $scope.config.disableToggle = false;
    +                }
    +                if ($scope.config.hasPassword === undefined) {
    +                    $scope.config.hasPassword = false;
    +                }
    +                if ($scope.config.enablePasswordRetrieval === undefined) {
    +                    $scope.config.enablePasswordRetrieval = true;
    +                }
    +                if ($scope.config.requiresQuestionAnswer === undefined) {
    +                    $scope.config.requiresQuestionAnswer = false;
    +                }
    +                //don't enable reset if it is new - that doesn't make sense
    +                if (isNew === 'true') {
    +                    $scope.config.enableReset = false;
    +                } else if ($scope.config.enableReset === undefined) {
    +                    $scope.config.enableReset = true;
    +                }
    +                if ($scope.config.minPasswordLength === undefined) {
    +                    $scope.config.minPasswordLength = 0;
    +                }
    +                //set the model defaults
    +                if (!angular.isObject($scope.passwordValues)) {
    +                    //if it's not an object then just create a new one
    +                    $scope.passwordValues = {
    +                        newPassword: null,
    +                        oldPassword: null,
    +                        reset: null,
    +                        answer: null
    +                    };
    +                } else {
    +                    //just reset the values
    +                    if (!isNew) {
    +                        //if it is new, then leave the generated pass displayed
    +                        $scope.passwordValues.newPassword = null;
    +                        $scope.passwordValues.oldPassword = null;
    +                    }
    +                    $scope.passwordValues.reset = null;
    +                    $scope.passwordValues.answer = null;
    +                }
    +                //the value to compare to match passwords
    +                if (!isNew) {
    +                    $scope.passwordValues.confirm = '';
    +                } else if ($scope.passwordValues.newPassword && $scope.passwordValues.newPassword.length > 0) {
    +                    //if it is new and a new password has been set, then set the confirm password too
    +                    $scope.passwordValues.confirm = $scope.passwordValues.newPassword;
    +                }
    +            }
    +            resetModel($scope.isNew);
    +            //if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there
    +            //with validators turned on.
    +            $scope.changing = $scope.config.disableToggle === true || !$scope.config.hasPassword;
    +            //we're not currently changing so set the model to null
    +            if (!$scope.changing) {
    +                $scope.passwordValues = null;
    +            }
    +            $scope.doChange = function () {
    +                resetModel();
    +                $scope.changing = true;
    +                //if there was a previously generated password displaying, clear it
    +                $scope.passwordValues.generatedPassword = null;
    +                $scope.passwordValues.confirm = null;
    +            };
    +            $scope.cancelChange = function () {
    +                $scope.changing = false;
    +                //set model to null
    +                $scope.passwordValues = null;
    +            };
    +            var unsubscribe = [];
    +            //listen for the saved event, when that occurs we'll 
    +            //change to changing = false;
    +            unsubscribe.push($scope.$on('formSubmitted', function () {
    +                if ($scope.config.disableToggle === false) {
    +                    $scope.changing = false;
    +                }
    +            }));
    +            unsubscribe.push($scope.$on('formSubmitting', function () {
    +                //if there was a previously generated password displaying, clear it
    +                if ($scope.changing && $scope.passwordValues) {
    +                    $scope.passwordValues.generatedPassword = null;
    +                } else if (!$scope.changing) {
    +                    //we are not changing, so the model needs to be null
    +                    $scope.passwordValues = null;
    +                }
    +            }));
    +            //when the scope is destroyed we need to unsubscribe
    +            $scope.$on('$destroy', function () {
    +                for (var u in unsubscribe) {
    +                    unsubscribe[u]();
    +                }
    +            });
    +            $scope.showOldPass = function () {
    +                return $scope.config.hasPassword && !$scope.config.allowManuallyChangingPassword && !$scope.config.enablePasswordRetrieval && !$scope.showReset;
    +            };
    +            //TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this directive
    +            $scope.showCancelBtn = function () {
    +                return $scope.config.disableToggle !== true && $scope.config.hasPassword;
    +            };
    +        }
    +        function ChangePasswordDirective() {
    +            var directive = {
    +                restrict: 'E',
    +                replace: true,
    +                templateUrl: 'views/components/users/change-password.html',
    +                controller: 'Umbraco.Editors.Users.ChangePasswordDirectiveController',
    +                scope: {
    +                    isNew: '=?',
    +                    passwordValues: '=',
    +                    config: '='
    +                }
                 };
    -
    -            vm.mouseOver = mouseOver;
    -            vm.mouseLeave = mouseLeave;
    -
    -            function mouseOver($event) {
    -                vm.tooltip = {
    -                    show: true,
    -                    event: $event
    -                };
    -            }
    -
    -            function mouseLeave() {
    -                vm.tooltip = {
    -                    show: false,
    -                    event: null
    +            return directive;
    +        }
    +        angular.module('umbraco.directives').controller('Umbraco.Editors.Users.ChangePasswordDirectiveController', ChangePasswordController);
    +        angular.module('umbraco.directives').directive('changePassword', ChangePasswordDirective);
    +    }());
    +    (function () {
    +        'use strict';
    +        function PermissionDirective() {
    +            function link(scope, el, attr, ctrl) {
    +                scope.change = function () {
    +                    scope.selected = !scope.selected;
    +                    if (scope.onChange) {
    +                        scope.onChange({ 'selected': scope.selected });
    +                    }
                     };
                 }
    -
    +            var directive = {
    +                restrict: 'E',
    +                replace: true,
    +                templateUrl: 'views/components/users/umb-permission.html',
    +                scope: {
    +                    name: '=',
    +                    description: '=?',
    +                    selected: '=',
    +                    onChange: '&'
    +                },
    +                link: link
    +            };
    +            return directive;
             }
    -
    -        angular.module("umbraco").controller("My.Controller", Controller);
    -
    -    })();
    -
    - -@param {string} event Set the $event from the target element to position the tooltip relative to the mouse cursor. + angular.module('umbraco.directives').directive('umbPermission', PermissionDirective); + }()); + /** +@ngdoc directive +@name umbraco.directives.directive:umbUserGroupPreview +@restrict E +@scope + +@description +Use this directive to render a user group preview, where you can see the permissions the user or group has in the back office. + +

    Markup example

    +
    +    
    + + +
    +
    + +@param {string} icon (binding): The user group icon. +@param {string} name (binding): The user group name. +@param {array} sections (binding) Lists out the sections where the user has authority to edit. +@param {string} contentStartNode (binding) +
      +
    • The starting point in the tree of the content section.
    • +
    • So the user has no authority to work on other branches, only on this branch in the content section.
    • +
    +@param {boolean} hideContentStartNode (binding) Hides the contentStartNode. +@param {string} mediaStartNode (binding) +
      +
    • The starting point in the tree of the media section.
    • +
    • So the user has no authority to work on other branches, only on this branch in the media section.
    • +
    +@param {boolean} hideMediaStartNode (binding) Hides the mediaStartNode. +@param {array} permissions (binding) A list of permissions, the user can have. +@param {boolean} allowRemove (binding): Shows or Hides the remove button. +@param {function} onRemove (expression): Callback function when the remove button is clicked. +@param {boolean} allowEdit (binding): Shows or Hides the edit button. +@param {function} onEdit (expression): Callback function when the edit button is clicked. **/ - -(function() { - 'use strict'; - - function TooltipDirective($timeout) { - - function link(scope, el, attr, ctrl) { - - scope.tooltipStyles = {}; - scope.tooltipStyles.left = 0; - scope.tooltipStyles.top = 0; - - function activate() { - - $timeout(function() { - setTooltipPosition(scope.event); - }); - - } - - function setTooltipPosition(event) { - - var container = $("#contentwrapper"); - var containerLeft = container[0].offsetLeft; - var containerRight = containerLeft + container[0].offsetWidth; - var containerTop = container[0].offsetTop; - var containerBottom = containerTop + container[0].offsetHeight; - - var elementHeight = null; - var elementWidth = null; - - var position = { - right: "inherit", - left: "inherit", - top: "inherit", - bottom: "inherit" - }; - - // element size - elementHeight = el.context.clientHeight; - elementWidth = el.context.clientWidth; - - position.left = event.pageX - (elementWidth / 2); - position.top = event.pageY; - - // check to see if element is outside screen - // outside right - if (position.left + elementWidth > containerRight) { - position.right = 10; - position.left = "inherit"; - } - - // outside bottom - if (position.top + elementHeight > containerBottom) { - position.bottom = 10; - position.top = "inherit"; - } - - // outside left - if (position.left < containerLeft) { - position.left = containerLeft + 10; - position.right = "inherit"; - } - - // outside top - if (position.top < containerTop) { - position.top = 10; - position.bottom = "inherit"; + (function () { + 'use strict'; + function UserGroupPreviewDirective() { + function link(scope, el, attr, ctrl) { } - - scope.tooltipStyles = position; - - el.css(position); - - } - - activate(); - - } - - var directive = { - restrict: 'E', - transclude: true, - replace: true, - templateUrl: 'views/components/umb-tooltip.html', - scope: { - event: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbTooltip', TooltipDirective); - -})(); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbFileDropzone -* @restrict E -* @function -* @description -* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. -**/ - -/* -TODO -.directive("umbFileDrop", function ($timeout, $upload, localizationService, umbRequestHelper){ - return{ - restrict: "A", - link: function(scope, element, attrs){ - //load in the options model + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/users/umb-user-group-preview.html', + scope: { + icon: '=?', + name: '=', + sections: '=?', + contentStartNode: '=?', + hideContentStartNode: '@?', + mediaStartNode: '=?', + hideMediaStartNode: '@?', + permissions: '=?', + allowRemove: '=?', + allowEdit: '=?', + onRemove: '&?', + onEdit: '&?' + }, + link: link + }; + return directive; } - } -}) -*/ - -angular.module("umbraco.directives") - .directive('umbFileDropzone', - function($timeout, Upload, localizationService, umbRequestHelper) { - return { + angular.module('umbraco.directives').directive('umbUserGroupPreview', UserGroupPreviewDirective); + }()); + (function () { + 'use strict'; + function UserPreviewDirective() { + function link(scope, el, attr, ctrl) { + } + var directive = { restrict: 'E', replace: true, - templateUrl: 'views/components/upload/umb-file-dropzone.html', + templateUrl: 'views/components/users/umb-user-preview.html', scope: { - parentId: '@', - contentTypeAlias: '@', - propertyAlias: '@', - accept: '@', - maxFileSize: '@', - - compact: '@', - hideDropzone: '@', - acceptedMediatypes: '=', - - filesQueued: '=', - handleFile: '=', - filesUploaded: '=' + avatars: '=?', + name: '=', + allowRemove: '=?', + onRemove: '&?' }, - link: function(scope, element, attrs) { - scope.queue = []; - scope.done = []; - scope.rejected = []; - scope.currentFile = undefined; - - function _filterFile(file) { - var ignoreFileNames = ['Thumbs.db']; - var ignoreFileTypes = ['directory']; - - // ignore files with names from the list - // ignore files with types from the list - // ignore files which starts with "." - if (ignoreFileNames.indexOf(file.name) === -1 && - ignoreFileTypes.indexOf(file.type) === -1 && - file.name.indexOf(".") !== 0) { - return true; - } else { - return false; - } - } - - function _filesQueued(files, event) { - //Push into the queue - angular.forEach(files, - function(file) { - - if (_filterFile(file) === true) { - - if (file.$error) { - scope.rejected.push(file); - } else { - scope.queue.push(file); - } - } - }); - - //when queue is done, kick the uploader - if (!scope.working) { - // Upload not allowed - if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) { - files.map(function(file) { - file.uploadStatus = "error"; - file.serverErrorMessage = "File type is not allowed here"; - scope.rejected.push(file); - }); - scope.queue = []; + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbUserPreview', UserPreviewDirective); + }()); + /** + * Konami Code directive for AngularJS + * @version v0.0.1 + * @license MIT License, http://www.opensource.org/licenses/MIT + */ + angular.module('umbraco.directives').directive('konamiCode', [ + '$document', + function ($document) { + var konamiKeysDefault = [ + 38, + 38, + 40, + 40, + 37, + 39, + 37, + 39, + 66, + 65 + ]; + return { + restrict: 'A', + link: function (scope, element, attr) { + if (!attr.konamiCode) { + throw 'Konami directive must receive an expression as value.'; + } + // Let user define a custom code. + var konamiKeys = attr.konamiKeys || konamiKeysDefault; + var keyIndex = 0; + /** + * Fired when konami code is type. + */ + function activated() { + if ('konamiOnce' in attr) { + stopListening(); + } + // Execute expression. + scope.$eval(attr.konamiCode); + } + /** + * Handle keydown events. + */ + function keydown(e) { + if (e.keyCode === konamiKeys[keyIndex++]) { + if (keyIndex === konamiKeys.length) { + keyIndex = 0; + activated(); } - // One allowed type - if (scope.acceptedMediatypes && scope.acceptedMediatypes.length === 1) { - // Standard setup - set alias to auto select to let the server best decide which media type to use - if (scope.acceptedMediatypes[0].alias === 'Image') { - scope.contentTypeAlias = "umbracoAutoSelect"; - } else { - scope.contentTypeAlias = scope.acceptedMediatypes[0].alias; - } - - _processQueueItem(); + } else { + keyIndex = 0; + } + } + /** + * Stop to listen typing. + */ + function stopListening() { + $document.off('keydown', keydown); + } + // Start listening to key typing. + $document.on('keydown', keydown); + // Stop listening when scope is destroyed. + scope.$on('$destroy', stopListening); + } + }; + } + ]); + /** +@ngdoc directive +@name umbraco.directives.directive:umbKeyboardList +@restrict E + +@description +Added in versions 7.7.0: Use this directive to add arrow up and down keyboard shortcuts to a list. Use this together with the {@link umbraco.directives.directive:umbDropdown umbDropdown} component to make easy accessible dropdown menus. + +

    Markup example

    +
    +    
    + +
    +
    + +

    Use in combination with

    +
      +
    • {@link umbraco.directives.directive:umbDropdown umbDropdown}
    • +
    + +**/ + angular.module('umbraco.directives').directive('umbKeyboardList', [ + '$document', + '$timeout', + function ($document, $timeout) { + return { + restrict: 'A', + link: function (scope, element, attr) { + var listItems = []; + var currentIndex = 0; + var focusSet = false; + $timeout(function () { + // get list of all links in the list + listItems = element.find('li a'); + }); + // Handle keydown events + function keydown(event) { + $timeout(function () { + checkFocus(); + // arrow down + if (event.keyCode === 40) { + arrowDown(); } - // More than one, open dialog - if (scope.acceptedMediatypes && scope.acceptedMediatypes.length > 1) { - _chooseMediaType(); + // arrow up + if (event.keyCode === 38) { + arrowUp(); } - } + }); } - - function _processQueueItem() { - if (scope.queue.length > 0) { - scope.currentFile = scope.queue.shift(); - _upload(scope.currentFile); - } else if (scope.done.length > 0) { - if (scope.filesUploaded) { - //queue is empty, trigger the done action - scope.filesUploaded(scope.done); + function checkFocus() { + var found = false; + // check if any element has focus + angular.forEach(listItems, function (item, index) { + if ($(item).is(':focus')) { + // if an element already has focus set the + // currentIndex so we navigate from that element + currentIndex = index; + focusSet = true; + found = true; } - - //auto-clear the done queue after 3 secs - var currentLength = scope.done.length; - $timeout(function() { - scope.done.splice(0, currentLength); - }, - 3000); - } - } - - function _upload(file) { - - scope.propertyAlias = scope.propertyAlias ? scope.propertyAlias : "umbracoFile"; - scope.contentTypeAlias = scope.contentTypeAlias ? scope.contentTypeAlias : "Image"; - - Upload.upload({ - url: umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostAddFile"), - fields: { - 'currentFolder': scope.parentId, - 'contentTypeAlias': scope.contentTypeAlias, - 'propertyAlias': scope.propertyAlias, - 'path': file.path - }, - file: file - }) - .progress(function(evt) { - // calculate progress in percentage - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); - // set percentage property on file - file.uploadProgress = progressPercentage; - // set uploading status on file - file.uploadStatus = "uploading"; - }) - .success(function(data, status, headers, config) { - if (data.notifications && data.notifications.length > 0) { - // set error status on file - file.uploadStatus = "error"; - // Throw message back to user with the cause of the error - file.serverErrorMessage = data.notifications[0].message; - // Put the file in the rejected pool - scope.rejected.push(file); - } else { - // set done status on file - file.uploadStatus = "done"; - // set date/time for when done - used for sorting - file.doneDate = new Date(); - // Put the file in the done pool - scope.done.push(file); - } - scope.currentFile = undefined; - //after processing, test if everthing is done - _processQueueItem(); - }) - .error(function(evt, status, headers, config) { - // set status done - file.uploadStatus = "error"; - //if the service returns a detailed error - if (evt.InnerException) { - file.serverErrorMessage = evt.InnerException.ExceptionMessage; - //Check if its the common "too large file" exception - if (evt.InnerException.StackTrace && - evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { - file.serverErrorMessage = "File too large to upload"; - } - } else if (evt.Message) { - file.serverErrorMessage = evt.Message; - } - // If file not found, server will return a 404 and display this message - if (status === 404) { - file.serverErrorMessage = "File not found"; - } - //after processing, test if everthing is done - scope.rejected.push(file); - scope.currentFile = undefined; - _processQueueItem(); - }); + }); + // If we don't find an element with focus we reset the currentIndex and the focusSet flag + // we do this because you can have navigated away from the list with tab and we want to reset it if you navigate back + if (!found) { + currentIndex = 0; + focusSet = false; + } } - - function _chooseMediaType() { - scope.mediatypepickerOverlay = { - view: "mediatypepicker", - title: "Choose media type", - acceptedMediatypes: scope.acceptedMediatypes, - hideSubmitButton: true, - show: true, - submit: function(model) { - scope.contentTypeAlias = model.selectedType.alias; - scope.mediatypepickerOverlay.show = false; - scope.mediatypepickerOverlay = null; - _processQueueItem(); - }, - close: function(oldModel) { - - scope.queue.map(function(file) { - file.uploadStatus = "error"; - file.serverErrorMessage = "Cannot upload this file, no mediatype selected"; - scope.rejected.push(file); - }); - scope.queue = []; - scope.mediatypepickerOverlay.show = false; - scope.mediatypepickerOverlay = null; + function arrowDown() { + if (currentIndex < listItems.length - 1) { + // only bump the current index if the focus is already + // set else we just want to focus the first element + if (focusSet) { + currentIndex++; } - }; + listItems[currentIndex].focus(); + focusSet = true; + } } - - scope.handleFiles = function(files, event) { - if (scope.filesQueued) { - scope.filesQueued(files, event); + function arrowUp() { + if (currentIndex > 0) { + currentIndex--; + listItems[currentIndex].focus(); } - _filesQueued(files, event); - }; + } + // Stop to listen typing. + function stopListening() { + $document.off('keydown', keydown); + } + // Start listening to key typing. + $document.on('keydown', keydown); + // Stop listening when scope is destroyed. + scope.$on('$destroy', stopListening); } }; - }); - -/** + } + ]); + /** * @ngdoc directive -* @name umbraco.directives.directive:umbFileUpload -* @function +* @name umbraco.directives.directive:noDirtyCheck * @restrict A -* @scope -* @description -* Listens for file input control changes and emits events when files are selected for use in other controllers. -**/ -function umbFileUpload() { - return { - restrict: "A", - scope: true, //create a new scope - link: function (scope, el, attrs) { - el.bind('change', function (event) { - var files = event.target.files; - //emit event upward - scope.$emit("filesSelected", { files: files }); - }); - } - }; -} - -angular.module('umbraco.directives').directive("umbFileUpload", umbFileUpload); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbSingleFileUpload -* @function -* @restrict A -* @scope -* @description -* A single file upload field that will reset itself based on the object passed in for the rebuild parameter. This -* is required because the only way to reset an upload control is to replace it's html. -**/ -function umbSingleFileUpload($compile) { - return { - restrict: "E", - scope: { - rebuild: "=" - }, - replace: true, - template: "
    ", - link: function (scope, el, attrs) { - - scope.$watch("rebuild", function (newVal, oldVal) { - if (newVal && newVal !== oldVal) { - //recompile it! - el.html(""); - $compile(el.contents())(scope); - } - }); - - } - }; -} - -angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload); -/** - * Konami Code directive for AngularJS - * @version v0.0.1 - * @license MIT License, http://www.opensource.org/licenses/MIT - */ - -angular.module('umbraco.directives') - .directive('konamiCode', ['$document', function ($document) { - var konamiKeysDefault = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]; - - return { - restrict: 'A', - link: function (scope, element, attr) { - - if (!attr.konamiCode) { - throw ('Konami directive must receive an expression as value.'); - } - - // Let user define a custom code. - var konamiKeys = attr.konamiKeys || konamiKeysDefault; - var keyIndex = 0; - - /** - * Fired when konami code is type. - */ - function activated() { - if ('konamiOnce' in attr) { - stopListening(); - } - // Execute expression. - scope.$eval(attr.konamiCode); - } - - /** - * Handle keydown events. - */ - function keydown(e) { - if (e.keyCode === konamiKeys[keyIndex++]) { - if (keyIndex === konamiKeys.length) { - keyIndex = 0; - activated(); - } - } else { - keyIndex = 0; - } - } - - /** - * Stop to listen typing. - */ - function stopListening() { - $document.off('keydown', keydown); - } - - // Start listening to key typing. - $document.on('keydown', keydown); - - // Stop listening when scope is destroyed. - scope.$on('$destroy', stopListening); - } - }; - }]); -/** -* @ngdoc directive -* @name umbraco.directives.directive:noDirtyCheck -* @restrict A -* @description Can be attached to form inputs to prevent them from setting the form as dirty (http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs) +* @description Can be attached to form inputs to prevent them from setting the form as dirty (http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs) **/ -function noDirtyCheck() { - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, elm, attrs, ctrl) { - elm.focus(function () { - ctrl.$pristine = false; - }); - } - }; -} -angular.module('umbraco.directives.validation').directive("noDirtyCheck", noDirtyCheck); -(function() { - 'use strict'; - - function SetDirtyOnChange() { - - function link(scope, el, attr, ctrl) { - - var initValue = attr.umbSetDirtyOnChange; - - attr.$observe("umbSetDirtyOnChange", function (newValue) { - if(newValue !== initValue) { - ctrl.$setDirty(); - } - }); - - } - - var directive = { - require: "^form", + function noDirtyCheck() { + return { restrict: 'A', - link: link + require: 'ngModel', + link: function (scope, elm, attrs, ctrl) { + elm.focus(function () { + ctrl.$pristine = false; + }); + } }; - - return directive; } - - angular.module('umbraco.directives').directive('umbSetDirtyOnChange', SetDirtyOnChange); - -})(); - -/** - * General-purpose validator for ngModel. - * angular.js comes with several built-in validation mechanism for input fields (ngRequired, ngPattern etc.) but using - * an arbitrary validation function requires creation of a custom formatters and / or parsers. - * The ui-validate directive makes it easy to use any function(s) defined in scope as a validator function(s). - * A validator function will trigger validation on both model and input changes. - * - * @example - * @example - * @example - * @example - * - * @param val-custom {string|object literal} If strings is passed it should be a scope's function to be used as a validator. - * If an object literal is passed a key denotes a validation error key while a value should be a validator function. - * In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result. + angular.module('umbraco.directives.validation').directive('noDirtyCheck', noDirtyCheck); + (function () { + 'use strict'; + function SetDirtyOnChange() { + function link(scope, el, attr, ctrl) { + if (attr.ngModel) { + scope.$watch(attr.ngModel, function (newValue, oldValue) { + if (!newValue) { + return; + } + if (newValue === oldValue) { + return; + } + ctrl.$setDirty(); + }, true); + } else { + var initValue = attr.umbSetDirtyOnChange; + attr.$observe('umbSetDirtyOnChange', function (newValue) { + if (newValue !== initValue) { + ctrl.$setDirty(); + } + }); + } + } + var directive = { + require: '^form', + restrict: 'A', + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbSetDirtyOnChange', SetDirtyOnChange); + }()); + angular.module('umbraco.directives.validation').directive('valCompare', function () { + return { + require: [ + 'ngModel', + '^form' + ], + link: function (scope, elem, attrs, ctrls) { + var ctrl = ctrls[0]; + var formCtrl = ctrls[1]; + var otherInput = formCtrl[attrs.valCompare]; + ctrl.$parsers.push(function (value) { + if (value === otherInput.$viewValue) { + ctrl.$setValidity('valCompare', true); + return value; + } + ctrl.$setValidity('valCompare', false); + }); + otherInput.$parsers.push(function (value) { + ctrl.$setValidity('valCompare', value === ctrl.$viewValue); + return value; + }); + } + }; + }); + /** + * General-purpose validator for ngModel. + * angular.js comes with several built-in validation mechanism for input fields (ngRequired, ngPattern etc.) but using + * an arbitrary validation function requires creation of a custom formatters and / or parsers. + * The ui-validate directive makes it easy to use any function(s) defined in scope as a validator function(s). + * A validator function will trigger validation on both model and input changes. + * + * @example + * @example + * @example + * @example + * + * @param val-custom {string|object literal} If strings is passed it should be a scope's function to be used as a validator. + * If an object literal is passed a key denotes a validation error key while a value should be a validator function. + * In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result. */ - - /* - This code comes from the angular UI project, we had to change the directive name and module - but other then that its unmodified + /* + This code comes from the angular UI project, we had to change the directive name and module + but other then that its unmodified */ -angular.module('umbraco.directives.validation') -.directive('valCustom', function () { - - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, elm, attrs, ctrl) { - var validateFn, watch, validators = {}, - validateExpr = scope.$eval(attrs.valCustom); - - if (!validateExpr){ return;} - - if (angular.isString(validateExpr)) { - validateExpr = { validator: validateExpr }; - } - - angular.forEach(validateExpr, function (exprssn, key) { - validateFn = function (valueToValidate) { - var expression = scope.$eval(exprssn, { '$value' : valueToValidate }); - if (angular.isObject(expression) && angular.isFunction(expression.then)) { - // expression is a promise - expression.then(function(){ - ctrl.$setValidity(key, true); - }, function(){ - ctrl.$setValidity(key, false); - }); - return valueToValidate; - } else if (expression) { - // expression is true - ctrl.$setValidity(key, true); - return valueToValidate; - } else { - // expression is false - ctrl.$setValidity(key, false); - return undefined; - } - }; - validators[key] = validateFn; - - ctrl.$parsers.push(validateFn); - }); - - function apply_watch(watch) - { - //string - update all validators on expression change - if (angular.isString(watch)) - { - scope.$watch(watch, function(){ - angular.forEach(validators, function(validatorFn){ - validatorFn(ctrl.$modelValue); - }); - }); - return; - } - - //array - update all validators on change of any expression - if (angular.isArray(watch)) - { - angular.forEach(watch, function(expression){ - scope.$watch(expression, function() - { - angular.forEach(validators, function(validatorFn){ - validatorFn(ctrl.$modelValue); - }); - }); - }); - return; - } - - //object - update appropriate validator - if (angular.isObject(watch)) - { - angular.forEach(watch, function(expression, validatorKey) - { - //value is string - look after one expression - if (angular.isString(expression)) - { - scope.$watch(expression, function(){ - validators[validatorKey](ctrl.$modelValue); - }); - } - - //value is array - look after all expressions in array - if (angular.isArray(expression)) - { - angular.forEach(expression, function(intExpression) - { - scope.$watch(intExpression, function(){ - validators[validatorKey](ctrl.$modelValue); - }); - }); - } - }); - } - } - // Support for val-custom-watch - if (attrs.valCustomWatch){ - apply_watch( scope.$eval(attrs.valCustomWatch) ); - } - } - }; -}); -/** -* @ngdoc directive -* @name umbraco.directives.directive:valHighlight -* @restrict A -* @description Used on input fields when you want to signal that they are in error, this will highlight the item for 1 second -**/ -function valHighlight($timeout) { - return { - restrict: "A", - link: function (scope, element, attrs, ctrl) { - - attrs.$observe("valHighlight", function (newVal) { - if (newVal === "true") { - element.addClass("highlight-error"); - $timeout(function () { - //set the bound scope property to false - scope[attrs.valHighlight] = false; - }, 1000); + angular.module('umbraco.directives.validation').directive('valCustom', function () { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, elm, attrs, ctrl) { + var validateFn, watch, validators = {}, validateExpr = scope.$eval(attrs.valCustom); + if (!validateExpr) { + return; + } + if (angular.isString(validateExpr)) { + validateExpr = { validator: validateExpr }; + } + angular.forEach(validateExpr, function (exprssn, key) { + validateFn = function (valueToValidate) { + var expression = scope.$eval(exprssn, { '$value': valueToValidate }); + if (angular.isObject(expression) && angular.isFunction(expression.then)) { + // expression is a promise + expression.then(function () { + ctrl.$setValidity(key, true); + }, function () { + ctrl.$setValidity(key, false); + }); + return valueToValidate; + } else if (expression) { + // expression is true + ctrl.$setValidity(key, true); + return valueToValidate; + } else { + // expression is false + ctrl.$setValidity(key, false); + return undefined; + } + }; + validators[key] = validateFn; + ctrl.$parsers.push(validateFn); + }); + function apply_watch(watch) { + //string - update all validators on expression change + if (angular.isString(watch)) { + scope.$watch(watch, function () { + angular.forEach(validators, function (validatorFn) { + validatorFn(ctrl.$modelValue); + }); + }); + return; + } + //array - update all validators on change of any expression + if (angular.isArray(watch)) { + angular.forEach(watch, function (expression) { + scope.$watch(expression, function () { + angular.forEach(validators, function (validatorFn) { + validatorFn(ctrl.$modelValue); + }); + }); + }); + return; + } + //object - update appropriate validator + if (angular.isObject(watch)) { + angular.forEach(watch, function (expression, validatorKey) { + //value is string - look after one expression + if (angular.isString(expression)) { + scope.$watch(expression, function () { + validators[validatorKey](ctrl.$modelValue); + }); + } + //value is array - look after all expressions in array + if (angular.isArray(expression)) { + angular.forEach(expression, function (intExpression) { + scope.$watch(intExpression, function () { + validators[validatorKey](ctrl.$modelValue); + }); + }); + } + }); + } } - else { - element.removeClass("highlight-error"); + // Support for val-custom-watch + if (attrs.valCustomWatch) { + apply_watch(scope.$eval(attrs.valCustomWatch)); } - }); - - } - }; -} -angular.module('umbraco.directives.validation').directive("valHighlight", valHighlight); - -angular.module('umbraco.directives.validation') - .directive('valCompare',function () { - return { - require: "ngModel", - link: function (scope, elem, attrs, ctrl) { - - //TODO: Pretty sure this should be done using a requires ^form in the directive declaration - var otherInput = elem.inheritedData("$formController")[attrs.valCompare]; - - ctrl.$parsers.push(function(value) { - if(value === otherInput.$viewValue) { - ctrl.$setValidity("valCompare", true); - return value; - } - ctrl.$setValidity("valCompare", false); - }); - - otherInput.$parsers.push(function(value) { - ctrl.$setValidity("valCompare", value === ctrl.$viewValue); - return value; - }); - } - }; -}); -/** - * @ngdoc directive - * @name umbraco.directives.directive:valEmail - * @restrict A - * @description A custom directive to validate an email address string, this is required because angular's default validator is incorrect. + } + }; + }); + /** + * @ngdoc directive + * @name umbraco.directives.directive:valEmail + * @restrict A + * @description A custom directive to validate an email address string, this is required because angular's default validator is incorrect. **/ -function valEmail(valEmailExpression) { - - return { - require: 'ngModel', - restrict: "A", - link: function (scope, elm, attrs, ctrl) { - - var patternValidator = function (viewValue) { - //NOTE: we don't validate on empty values, use required validator for that - if (!viewValue || valEmailExpression.EMAIL_REGEXP.test(viewValue)) { - // it is valid - ctrl.$setValidity('valEmail', true); - //assign a message to the validator - ctrl.errorMsg = ""; - return viewValue; - } - else { - // it is invalid, return undefined (no model update) - ctrl.$setValidity('valEmail', false); - //assign a message to the validator - ctrl.errorMsg = "Invalid email"; - return undefined; + function valEmail(valEmailExpression) { + return { + require: 'ngModel', + restrict: 'A', + link: function (scope, elm, attrs, ctrl) { + var patternValidator = function (viewValue) { + //NOTE: we don't validate on empty values, use required validator for that + if (!viewValue || valEmailExpression.EMAIL_REGEXP.test(viewValue)) { + // it is valid + ctrl.$setValidity('valEmail', true); + //assign a message to the validator + ctrl.errorMsg = ''; + return viewValue; + } else { + // it is invalid, return undefined (no model update) + ctrl.$setValidity('valEmail', false); + //assign a message to the validator + ctrl.errorMsg = 'Invalid email'; + return undefined; + } + }; + //if there is an attribute: type="email" then we need to remove those formatters and parsers + if (attrs.type === 'email') { + //we need to remove the existing parsers = the default angular one which is created by + // type="email", but this has a regex issue, so we'll remove that and add our custom one + ctrl.$parsers.pop(); + //we also need to remove the existing formatter - the default angular one will not render + // what it thinks is an invalid email address, so it will just be blank + ctrl.$formatters.pop(); } - }; - - //if there is an attribute: type="email" then we need to remove those formatters and parsers - if (attrs.type === "email") { - //we need to remove the existing parsers = the default angular one which is created by - // type="email", but this has a regex issue, so we'll remove that and add our custom one - ctrl.$parsers.pop(); - //we also need to remove the existing formatter - the default angular one will not render - // what it thinks is an invalid email address, so it will just be blank - ctrl.$formatters.pop(); + ctrl.$parsers.push(patternValidator); } - - ctrl.$parsers.push(patternValidator); - } - }; -} - -angular.module('umbraco.directives.validation') - .directive("valEmail", valEmail) - .factory('valEmailExpression', function () { - //NOTE: This is the fixed regex which is part of the newer angular - return { - EMAIL_REGEXP: /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i }; - }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:valFormManager -* @restrict A -* @require formController -* @description Used to broadcast an event to all elements inside this one to notify that form validation has -* changed. If we don't use this that means you have to put a watch for each directive on a form's validation -* changing which would result in much higher processing. We need to actually watch the whole $error collection of a form -* because just watching $valid or $invalid doesn't acurrately trigger form validation changing. -* This also sets the show-validation (or a custom) css class on the element when the form is invalid - this lets -* us css target elements to be displayed when the form is submitting/submitted. -* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will -* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. + } + angular.module('umbraco.directives.validation').directive('valEmail', valEmail).factory('valEmailExpression', function () { + var emailRegex = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; + return { EMAIL_REGEXP: emailRegex }; + }); + /** +* @ngdoc directive +* @name umbraco.directives.directive:valFormManager +* @restrict A +* @require formController +* @description Used to broadcast an event to all elements inside this one to notify that form validation has +* changed. If we don't use this that means you have to put a watch for each directive on a form's validation +* changing which would result in much higher processing. We need to actually watch the whole $error collection of a form +* because just watching $valid or $invalid doesn't acurrately trigger form validation changing. +* This also sets the show-validation (or a custom) css class on the element when the form is invalid - this lets +* us css target elements to be displayed when the form is submitting/submitted. +* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will +* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. **/ -function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) { - return { - require: "form", - restrict: "A", - controller: function($scope) { - //This exposes an API for direct use with this directive - - var unsubscribe = []; - var self = this; - - //This is basically the same as a directive subscribing to an event but maybe a little - // nicer since the other directive can use this directive's API instead of a magical event - this.onValidationStatusChanged = function (cb) { - unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) { - cb.apply(self, [evt, args]); + function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) { + return { + require: 'form', + restrict: 'A', + controller: function ($scope) { + //This exposes an API for direct use with this directive + var unsubscribe = []; + var self = this; + //This is basically the same as a directive subscribing to an event but maybe a little + // nicer since the other directive can use this directive's API instead of a magical event + this.onValidationStatusChanged = function (cb) { + unsubscribe.push($scope.$on('valStatusChanged', function (evt, args) { + cb.apply(self, [ + evt, + args + ]); + })); + }; + //Ensure to remove the event handlers when this instance is destroyted + $scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + }, + link: function (scope, element, attr, formCtrl) { + scope.$watch(function () { + return formCtrl.$error; + }, function (e) { + scope.$broadcast('valStatusChanged', { form: formCtrl }); + //find all invalid elements' .control-group's and apply the error class + var inError = element.find('.control-group .ng-invalid').closest('.control-group'); + inError.addClass('error'); + //find all control group's that have no error and ensure the class is removed + var noInError = element.find('.control-group .ng-valid').closest('.control-group').not(inError); + noInError.removeClass('error'); + }, true); + var className = attr.valShowValidation ? attr.valShowValidation : 'show-validation'; + var savingEventName = attr.savingEvent ? attr.savingEvent : 'formSubmitting'; + var savedEvent = attr.savedEvent ? attr.savingEvent : 'formSubmitted'; + //This tracks if the user is currently saving a new item, we use this to determine + // if we should display the warning dialog that they are leaving the page - if a new item + // is being saved we never want to display that dialog, this will also cause problems when there + // are server side validation issues. + var isSavingNewItem = false; + //we should show validation if there are any msgs in the server validation collection + if (serverValidationManager.items.length > 0) { + element.addClass(className); + } + var unsubscribe = []; + //listen for the forms saving event + unsubscribe.push(scope.$on(savingEventName, function (ev, args) { + element.addClass(className); + //set the flag so we can check to see if we should display the error. + isSavingNewItem = $routeParams.create; })); - }; - - //Ensure to remove the event handlers when this instance is destroyted - $scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - }, - link: function (scope, element, attr, formCtrl) { - - scope.$watch(function () { - return formCtrl.$error; - }, function (e) { - scope.$broadcast("valStatusChanged", { form: formCtrl }); - - //find all invalid elements' .control-group's and apply the error class - var inError = element.find(".control-group .ng-invalid").closest(".control-group"); - inError.addClass("error"); - - //find all control group's that have no error and ensure the class is removed - var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError); - noInError.removeClass("error"); - - }, true); - - var className = attr.valShowValidation ? attr.valShowValidation : "show-validation"; - var savingEventName = attr.savingEvent ? attr.savingEvent : "formSubmitting"; - var savedEvent = attr.savedEvent ? attr.savingEvent : "formSubmitted"; - - //This tracks if the user is currently saving a new item, we use this to determine - // if we should display the warning dialog that they are leaving the page - if a new item - // is being saved we never want to display that dialog, this will also cause problems when there - // are server side validation issues. - var isSavingNewItem = false; - - //we should show validation if there are any msgs in the server validation collection - if (serverValidationManager.items.length > 0) { - element.addClass(className); - } - - var unsubscribe = []; - - //listen for the forms saving event - unsubscribe.push(scope.$on(savingEventName, function(ev, args) { - element.addClass(className); - - //set the flag so we can check to see if we should display the error. - isSavingNewItem = $routeParams.create; - })); - - //listen for the forms saved event - unsubscribe.push(scope.$on(savedEvent, function(ev, args) { - //remove validation class - element.removeClass(className); - - //clear form state as at this point we retrieve new data from the server - //and all validation will have cleared at this point - formCtrl.$setPristine(); - })); - - //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but - // the form has pending changes - var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) { - if (!formCtrl.$dirty || isSavingNewItem) { - return; - } - - var path = nextLocation.split("#")[1]; - if (path) { - if (path.indexOf("%253") || path.indexOf("%252")) { - path = decodeURIComponent(path); + //listen for the forms saved event + unsubscribe.push(scope.$on(savedEvent, function (ev, args) { + //remove validation class + element.removeClass(className); + //clear form state as at this point we retrieve new data from the server + //and all validation will have cleared at this point + formCtrl.$setPristine(); + })); + //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but + // the form has pending changes + var locationEvent = $rootScope.$on('$locationChangeStart', function (event, nextLocation, currentLocation) { + if (!formCtrl.$dirty || isSavingNewItem) { + return; } - - if (!notificationsService.hasView()) { - var msg = { view: "confirmroutechange", args: { path: path, listener: locationEvent } }; - notificationsService.add(msg); + var path = nextLocation.split('#')[1]; + if (path) { + if (path.indexOf('%253') || path.indexOf('%252')) { + path = decodeURIComponent(path); + } + if (!notificationsService.hasView()) { + var msg = { + view: 'confirmroutechange', + args: { + path: path, + listener: locationEvent + } + }; + notificationsService.add(msg); + } + //prevent the route! + event.preventDefault(); + //raise an event + eventsService.emit('valFormManager.pendingChanges', true); } - - //prevent the route! - event.preventDefault(); - - //raise an event - eventsService.emit("valFormManager.pendingChanges", true); - } - - }); - unsubscribe.push(locationEvent); - - //Ensure to remove the event handler when this instance is destroyted - scope.$on('$destroy', function() { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - - $timeout(function(){ - formCtrl.$setPristine(); - }, 1000); - } - }; -} -angular.module('umbraco.directives.validation').directive("valFormManager", valFormManager); -/** -* @ngdoc directive -* @name umbraco.directives.directive:valPropertyMsg -* @restrict A -* @element textarea -* @requires formController -* @description This directive is used to control the display of the property level validation message. -* We will listen for server side validation changes -* and when an error is detected for this property we'll show the error message. -* In order for this directive to work, the valStatusChanged directive must be placed on the containing form. + }); + unsubscribe.push(locationEvent); + //Ensure to remove the event handler when this instance is destroyted + scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + $timeout(function () { + formCtrl.$setPristine(); + }, 1000); + } + }; + } + angular.module('umbraco.directives.validation').directive('valFormManager', valFormManager); + /** +* @ngdoc directive +* @name umbraco.directives.directive:valHighlight +* @restrict A +* @description Used on input fields when you want to signal that they are in error, this will highlight the item for 1 second +**/ + function valHighlight($timeout) { + return { + restrict: 'A', + link: function (scope, element, attrs, ctrl) { + attrs.$observe('valHighlight', function (newVal) { + if (newVal === 'true') { + element.addClass('highlight-error'); + $timeout(function () { + //set the bound scope property to false + scope[attrs.valHighlight] = false; + }, 1000); + } else { + element.removeClass('highlight-error'); + } + }); + } + }; + } + angular.module('umbraco.directives.validation').directive('valHighlight', valHighlight); + /** +* @ngdoc directive +* @name umbraco.directives.directive:valPropertyMsg +* @restrict A +* @element textarea +* @requires formController +* @description This directive is used to control the display of the property level validation message. +* We will listen for server side validation changes +* and when an error is detected for this property we'll show the error message. +* In order for this directive to work, the valStatusChanged directive must be placed on the containing form. **/ -function valPropertyMsg(serverValidationManager) { - - return { - scope: { - property: "=" - }, - require: "^form", //require that this directive is contained within an ngForm - replace: true, //replace the element with the template - restrict: "E", //restrict to element - template: "
    {{errorMsg}}
    ", - - /** - Our directive requries a reference to a form controller - which gets passed in to this parameter + function valPropertyMsg(serverValidationManager) { + return { + scope: { property: '=' }, + require: '^form', + //require that this directive is contained within an ngForm + replace: true, + //replace the element with the template + restrict: 'E', + //restrict to element + template: '
    {{errorMsg}}
    ', + /** + Our directive requries a reference to a form controller + which gets passed in to this parameter */ - link: function (scope, element, attrs, formCtrl) { - - var watcher = null; - - // Gets the error message to display - function getErrorMsg() { - //this can be null if no property was assigned - if (scope.property) { - //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(scope.property.alias, ""); - //if there's an error message use it - if (err && err.errorMsg) { - return err.errorMsg; + link: function (scope, element, attrs, formCtrl) { + var watcher = null; + // Gets the error message to display + function getErrorMsg() { + //this can be null if no property was assigned + if (scope.property) { + //first try to get the error msg from the server collection + var err = serverValidationManager.getPropertyError(scope.property.alias, ''); + //if there's an error message use it + if (err && err.errorMsg) { + return err.errorMsg; + } else { + return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : 'Property has errors'; + } } - else { - return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : "Property has errors"; + return 'Property has errors'; + } + // We need to subscribe to any changes to our model (based on user input) + // This is required because when we have a server error we actually invalidate + // the form which means it cannot be resubmitted. + // So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + function startWatch() { + //if there's not already a watch + if (!watcher) { + watcher = scope.$watch('property.value', function (newValue, oldValue) { + if (!newValue || angular.equals(newValue, oldValue)) { + return; + } + var errCount = 0; + for (var e in formCtrl.$error) { + if (angular.isArray(formCtrl.$error[e])) { + errCount++; + } + } + //we are explicitly checking for valServer errors here, since we shouldn't auto clear + // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg + // is the only one, then we'll clear. + if (errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg) || formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer)) { + scope.errorMsg = ''; + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + } + }, true); } - } - return "Property has errors"; - } - - // We need to subscribe to any changes to our model (based on user input) - // This is required because when we have a server error we actually invalidate - // the form which means it cannot be resubmitted. - // So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - function startWatch() { - //if there's not already a watch - if (!watcher) { - watcher = scope.$watch("property.value", function (newValue, oldValue) { - - if (!newValue || angular.equals(newValue, oldValue)) { + //clear the watch when the property validator is valid again + function stopWatch() { + if (watcher) { + watcher(); + watcher = null; + } + } + //if there's any remaining errors in the server validation service then we should show them. + var showValidation = serverValidationManager.items.length > 0; + var hasError = false; + //create properties on our custom scope so we can use it in our template + scope.errorMsg = ''; + var unsubscribe = []; + //listen for form error changes + unsubscribe.push(scope.$on('valStatusChanged', function (evt, args) { + if (args.form.$invalid) { + //first we need to check if the valPropertyMsg validity is invalid + if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { + //since we already have an error we'll just return since this means we've already set the + // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe return; - } - - var errCount = 0; - for (var e in formCtrl.$error) { - if (angular.isArray(formCtrl.$error[e])) { - errCount++; + } else if (element.closest('.umb-control-group').find('.ng-invalid').length > 0) { + //check if it's one of the properties that is invalid in the current content property + hasError = true; + //update the validation message if we don't already have one assigned. + if (showValidation && scope.errorMsg === '') { + scope.errorMsg = getErrorMsg(); } + } else { + hasError = false; + scope.errorMsg = ''; } - - //we are explicitly checking for valServer errors here, since we shouldn't auto clear - // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg - // is the only one, then we'll clear. - - if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { - scope.errorMsg = ""; + } else { + hasError = false; + scope.errorMsg = ''; + } + }, true)); + //listen for the forms saving event + unsubscribe.push(scope.$on('formSubmitting', function (ev, args) { + showValidation = true; + if (hasError && scope.errorMsg === '') { + scope.errorMsg = getErrorMsg(); + } else if (!hasError) { + scope.errorMsg = ''; + stopWatch(); + } + })); + //listen for the forms saved event + unsubscribe.push(scope.$on('formSubmitted', function (ev, args) { + showValidation = false; + scope.errorMsg = ''; + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + })); + //listen for server validation changes + // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for + // validation changes to fields in the property this is because some server side validators may not + // return the field name for which the error belongs too, just the property for which it belongs. + // It's important to note that we need to subscribe to server validation changes here because we always must + // indicate that a content property is invalid at the property level since developers may not actually implement + // the correct field validation in their property editors. + if (scope.property) { + //this can be null if no property was assigned + serverValidationManager.subscribe(scope.property.alias, '', function (isValid, propertyErrors, allErrors) { + hasError = !isValid; + if (hasError) { + //set the error message to the server message + scope.errorMsg = propertyErrors[0].errorMsg; + //flag that the current validator is invalid + formCtrl.$setValidity('valPropertyMsg', false); + startWatch(); + } else { + scope.errorMsg = ''; + //flag that the current validator is valid formCtrl.$setValidity('valPropertyMsg', true); stopWatch(); } - }, true); + }); + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + stopWatch(); + serverValidationManager.unsubscribe(scope.property.alias, ''); + }); } + //when the scope is disposed we need to unsubscribe + scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); } - - //clear the watch when the property validator is valid again - function stopWatch() { - if (watcher) { - watcher(); - watcher = null; - } + }; + } + angular.module('umbraco.directives.validation').directive('valPropertyMsg', valPropertyMsg); + /** +* @ngdoc directive +* @name umbraco.directives.directive:valPropertyValidator +* @restrict A +* @description Performs any custom property value validation checks on the client side. This allows property editors to be highly flexible when it comes to validation + on the client side. Typically if a property editor stores a primitive value (i.e. string) then the client side validation can easily be taken care of + with standard angular directives such as ng-required. However since some property editors store complex data such as JSON, a given property editor + might require custom validation. This directive can be used to validate an Umbraco property in any way that a developer would like by specifying a + callback method to perform the validation. The result of this method must return an object in the format of + {isValid: true, errorKey: 'required', errorMsg: 'Something went wrong' } + The error message returned will also be displayed for the property level validation message. + This directive should only be used when dealing with complex models, if custom validation needs to be performed with primitive values, use the simpler + angular validation directives instead since this will watch the entire model. +**/ + function valPropertyValidator(serverValidationManager) { + return { + scope: { valPropertyValidator: '=' }, + // The element must have ng-model attribute and be inside an umbProperty directive + require: [ + 'ngModel', + '?^umbProperty' + ], + restrict: 'A', + link: function (scope, element, attrs, ctrls) { + var modelCtrl = ctrls[0]; + var propCtrl = ctrls.length > 1 ? ctrls[1] : null; + // Check whether the scope has a valPropertyValidator method + if (!scope.valPropertyValidator || !angular.isFunction(scope.valPropertyValidator)) { + throw new Error('val-property-validator directive must specify a function to call'); + } + var initResult = scope.valPropertyValidator(); + // Validation method + var validate = function (viewValue) { + // Calls the validition method + var result = scope.valPropertyValidator(); + if (!result.errorKey || result.isValid === undefined || !result.errorMsg) { + throw 'The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg'; + } + if (result.isValid === true) { + // Tell the controller that the value is valid + modelCtrl.$setValidity(result.errorKey, true); + if (propCtrl) { + propCtrl.setPropertyError(null); + } + } else { + // Tell the controller that the value is invalid + modelCtrl.$setValidity(result.errorKey, false); + if (propCtrl) { + propCtrl.setPropertyError(result.errorMsg); + } + } + }; + // Parsers are called as soon as the value in the form input is modified + modelCtrl.$parsers.push(validate); } - - //if there's any remaining errors in the server validation service then we should show them. - var showValidation = serverValidationManager.items.length > 0; - var hasError = false; - - //create properties on our custom scope so we can use it in our template - scope.errorMsg = ""; - - var unsubscribe = []; - - //listen for form error changes - unsubscribe.push(scope.$on("valStatusChanged", function(evt, args) { - if (args.form.$invalid) { - - //first we need to check if the valPropertyMsg validity is invalid - if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { - //since we already have an error we'll just return since this means we've already set the - // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe - return; + }; + } + angular.module('umbraco.directives.validation').directive('valPropertyValidator', valPropertyValidator); + /** + * @ngdoc directive + * @name umbraco.directives.directive:valRegex + * @restrict A + * @description A custom directive to allow for matching a value against a regex string. + * NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string + **/ + function valRegex() { + return { + require: 'ngModel', + restrict: 'A', + link: function (scope, elm, attrs, ctrl) { + var flags = ''; + var regex; + var eventBindings = []; + attrs.$observe('valRegexFlags', function (newVal) { + if (newVal) { + flags = newVal; + } + }); + attrs.$observe('valRegex', function (newVal) { + if (newVal) { + try { + var resolved = newVal; + if (resolved) { + regex = new RegExp(resolved, flags); + } else { + regex = new RegExp(attrs.valRegex, flags); + } + } catch (e) { + regex = new RegExp(attrs.valRegex, flags); + } + } + }); + eventBindings.push(scope.$watch('ngModel', function (newValue, oldValue) { + if (newValue && newValue !== oldValue) { + patternValidator(newValue); } - else if (element.closest(".umb-control-group").find(".ng-invalid").length > 0) { - //check if it's one of the properties that is invalid in the current content property - hasError = true; - //update the validation message if we don't already have one assigned. - if (showValidation && scope.errorMsg === "") { - scope.errorMsg = getErrorMsg(); + })); + var patternValidator = function (viewValue) { + if (regex) { + //NOTE: we don't validate on empty values, use required validator for that + if (!viewValue || regex.test(viewValue.toString())) { + // it is valid + ctrl.$setValidity('valRegex', true); + //assign a message to the validator + ctrl.errorMsg = ''; + return viewValue; + } else { + // it is invalid, return undefined (no model update) + ctrl.$setValidity('valRegex', false); + //assign a message to the validator + ctrl.errorMsg = 'Value is invalid, it does not match the correct pattern'; + return undefined; } } - else { - hasError = false; - scope.errorMsg = ""; + }; + scope.$on('$destroy', function () { + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } + }); + } + }; + } + angular.module('umbraco.directives.validation').directive('valRegex', valRegex); + (function () { + 'use strict'; + function ValRequireComponentDirective() { + function link(scope, el, attr, ngModel) { + var unbindModelWatcher = scope.$watch(function () { + return ngModel.$modelValue; + }, function (newValue) { + if (newValue === undefined || newValue === null || newValue === '') { + ngModel.$setValidity('valRequiredComponent', false); + } else { + ngModel.$setValidity('valRequiredComponent', true); } + }); + // clean up + scope.$on('$destroy', function () { + unbindModelWatcher(); + }); + } + var directive = { + require: 'ngModel', + restrict: 'A', + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('valRequireComponent', ValRequireComponentDirective); + }()); + /** + * @ngdoc directive + * @name umbraco.directives.directive:valServer + * @restrict A + * @description This directive is used to associate a content property with a server-side validation response + * so that the validators in angular are updated based on server-side feedback. + **/ + function valServer(serverValidationManager) { + return { + require: [ + 'ngModel', + '?^umbProperty' + ], + restrict: 'A', + link: function (scope, element, attr, ctrls) { + var modelCtrl = ctrls[0]; + var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null; + if (!umbPropCtrl) { + //we cannot proceed, this validator will be disabled + return; } - else { - hasError = false; - scope.errorMsg = ""; + var watcher = null; + //Need to watch the value model for it to change, previously we had subscribed to + //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that + // doesn't specifically have a 2 way ng binding. This is required because when we + // have a server error we actually invalidate the form which means it cannot be + // resubmitted. So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + function startWatch() { + //if there's not already a watch + if (!watcher) { + watcher = scope.$watch(function () { + return modelCtrl.$modelValue; + }, function (newValue, oldValue) { + if (!newValue || angular.equals(newValue, oldValue)) { + return; + } + if (modelCtrl.$invalid) { + modelCtrl.$setValidity('valServer', true); + stopWatch(); + } + }, true); + } } - }, true)); - - //listen for the forms saving event - unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { - showValidation = true; - if (hasError && scope.errorMsg === "") { - scope.errorMsg = getErrorMsg(); + function stopWatch() { + if (watcher) { + watcher(); + watcher = null; + } } - else if (!hasError) { - scope.errorMsg = ""; - stopWatch(); + var currentProperty = umbPropCtrl.property; + //default to 'value' if nothing is set + var fieldName = 'value'; + if (attr.valServer) { + fieldName = scope.$eval(attr.valServer); + if (!fieldName) { + //eval returned nothing so just use the string + fieldName = attr.valServer; + } } - })); - - //listen for the forms saved event - unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { - showValidation = false; - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); - })); - - //listen for server validation changes - // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for - // validation changes to fields in the property this is because some server side validators may not - // return the field name for which the error belongs too, just the property for which it belongs. - // It's important to note that we need to subscribe to server validation changes here because we always must - // indicate that a content property is invalid at the property level since developers may not actually implement - // the correct field validation in their property editors. - - if (scope.property) { //this can be null if no property was assigned - serverValidationManager.subscribe(scope.property.alias, "", function (isValid, propertyErrors, allErrors) { - hasError = !isValid; - if (hasError) { - //set the error message to the server message - scope.errorMsg = propertyErrors[0].errorMsg; - //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false); + //subscribe to the server validation changes + serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) { + if (!isValid) { + modelCtrl.$setValidity('valServer', false); + //assign an error msg property to the current validator + modelCtrl.errorMsg = propertyErrors[0].errorMsg; startWatch(); - } - else { - scope.errorMsg = ""; - //flag that the current validator is valid - formCtrl.$setValidity('valPropertyMsg', true); + } else { + modelCtrl.$setValidity('valServer', true); + //reset the error message + modelCtrl.errorMsg = ''; stopWatch(); } }); - //when the element is disposed we need to unsubscribe! // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain // but they are a different callback instance than the above. element.bind('$destroy', function () { stopWatch(); - serverValidationManager.unsubscribe(scope.property.alias, ""); + serverValidationManager.unsubscribe(currentProperty.alias, fieldName); }); } - - //when the scope is disposed we need to unsubscribe - scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - } - - - }; -} -angular.module('umbraco.directives.validation').directive("valPropertyMsg", valPropertyMsg); -/** -* @ngdoc directive -* @name umbraco.directives.directive:valPropertyValidator -* @restrict A -* @description Performs any custom property value validation checks on the client side. This allows property editors to be highly flexible when it comes to validation - on the client side. Typically if a property editor stores a primitive value (i.e. string) then the client side validation can easily be taken care of - with standard angular directives such as ng-required. However since some property editors store complex data such as JSON, a given property editor - might require custom validation. This directive can be used to validate an Umbraco property in any way that a developer would like by specifying a - callback method to perform the validation. The result of this method must return an object in the format of - {isValid: true, errorKey: 'required', errorMsg: 'Something went wrong' } - The error message returned will also be displayed for the property level validation message. - This directive should only be used when dealing with complex models, if custom validation needs to be performed with primitive values, use the simpler - angular validation directives instead since this will watch the entire model. -**/ - -function valPropertyValidator(serverValidationManager) { - return { - scope: { - valPropertyValidator: "=" - }, - - // The element must have ng-model attribute and be inside an umbProperty directive - require: ['ngModel', '?^umbProperty'], - - restrict: "A", - - link: function (scope, element, attrs, ctrls) { - - var modelCtrl = ctrls[0]; - var propCtrl = ctrls.length > 1 ? ctrls[1] : null; - - // Check whether the scope has a valPropertyValidator method - if (!scope.valPropertyValidator || !angular.isFunction(scope.valPropertyValidator)) { - throw new Error('val-property-validator directive must specify a function to call'); - } - - var initResult = scope.valPropertyValidator(); - - // Validation method - var validate = function (viewValue) { - // Calls the validition method - var result = scope.valPropertyValidator(); - if (!result.errorKey || result.isValid === undefined || !result.errorMsg) { - throw "The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg"; - } - if (result.isValid === true) { - // Tell the controller that the value is valid - modelCtrl.$setValidity(result.errorKey, true); - if (propCtrl) { - propCtrl.setPropertyError(null); - } - } - else { - // Tell the controller that the value is invalid - modelCtrl.$setValidity(result.errorKey, false); - if (propCtrl) { - propCtrl.setPropertyError(result.errorMsg); - } - } - }; - - // Parsers are called as soon as the value in the form input is modified - modelCtrl.$parsers.push(validate); - - } - }; -} -angular.module('umbraco.directives.validation').directive("valPropertyValidator", valPropertyValidator); - -/** - * @ngdoc directive - * @name umbraco.directives.directive:valRegex - * @restrict A - * @description A custom directive to allow for matching a value against a regex string. - * NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string + }; + } + angular.module('umbraco.directives.validation').directive('valServer', valServer); + /** + * @ngdoc directive + * @name umbraco.directives.directive:valServerField + * @restrict A + * @description This directive is used to associate a content field (not user defined) with a server-side validation response + * so that the validators in angular are updated based on server-side feedback. **/ -function valRegex() { - - return { - require: 'ngModel', - restrict: "A", - link: function (scope, elm, attrs, ctrl) { - - var flags = ""; - var regex; - var eventBindings = []; - - attrs.$observe("valRegexFlags", function (newVal) { - if (newVal) { - flags = newVal; - } - }); - - attrs.$observe("valRegex", function (newVal) { - if (newVal) { - try { - var resolved = newVal; - if (resolved) { - regex = new RegExp(resolved, flags); - } - else { - regex = new RegExp(attrs.valRegex, flags); - } - } - catch (e) { - regex = new RegExp(attrs.valRegex, flags); - } - } - }); - - eventBindings.push(scope.$watch('ngModel', function(newValue, oldValue){ - if(newValue && newValue !== oldValue) { - patternValidator(newValue); - } - })); - - var patternValidator = function (viewValue) { - if (regex) { - //NOTE: we don't validate on empty values, use required validator for that - if (!viewValue || regex.test(viewValue.toString())) { - // it is valid - ctrl.$setValidity('valRegex', true); - //assign a message to the validator - ctrl.errorMsg = ""; - return viewValue; + function valServerField(serverValidationManager) { + return { + require: 'ngModel', + restrict: 'A', + link: function (scope, element, attr, ngModel) { + var fieldName = null; + var eventBindings = []; + attr.$observe('valServerField', function (newVal) { + if (newVal && fieldName === null) { + fieldName = newVal; + //subscribe to the changed event of the view model. This is required because when we + // have a server error we actually invalidate the form which means it cannot be + // resubmitted. So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + eventBindings.push(scope.$watch(function () { + return ngModel.$modelValue; + }, function (newValue) { + if (ngModel.$invalid) { + ngModel.$setValidity('valServerField', true); + } + })); + //subscribe to the server validation changes + serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { + if (!isValid) { + ngModel.$setValidity('valServerField', false); + //assign an error msg property to the current validator + ngModel.errorMsg = fieldErrors[0].errorMsg; + } else { + ngModel.$setValidity('valServerField', true); + //reset the error message + ngModel.errorMsg = ''; + } + }); + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + serverValidationManager.unsubscribe(null, fieldName); + }); } - else { - // it is invalid, return undefined (no model update) - ctrl.$setValidity('valRegex', false); - //assign a message to the validator - ctrl.errorMsg = "Value is invalid, it does not match the correct pattern"; - return undefined; + }); + scope.$on('$destroy', function () { + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); } - } - }; - - scope.$on('$destroy', function(){ - // unbind watchers - for(var e in eventBindings) { - eventBindings[e](); - } - }); - - } - }; -} -angular.module('umbraco.directives.validation').directive("valRegex", valRegex); - -(function() { - 'use strict'; - - function ValRequireComponentDirective() { - - function link(scope, el, attr, ngModel) { - - var unbindModelWatcher = scope.$watch(function () { - return ngModel.$modelValue; - }, function(newValue) { - - if(newValue === undefined || newValue === null || newValue === "") { - ngModel.$setValidity("valRequiredComponent", false); - } else { - ngModel.$setValidity("valRequiredComponent", true); - } - - }); - - // clean up - scope.$on('$destroy', function(){ - unbindModelWatcher(); - }); - - } - - var directive = { - require: 'ngModel', - restrict: "A", - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('valRequireComponent', ValRequireComponentDirective); - -})(); - -/** - * @ngdoc directive - * @name umbraco.directives.directive:valServer - * @restrict A - * @description This directive is used to associate a content property with a server-side validation response - * so that the validators in angular are updated based on server-side feedback. - **/ -function valServer(serverValidationManager) { - return { - require: ['ngModel', '?^umbProperty'], - restrict: "A", - link: function (scope, element, attr, ctrls) { - - var modelCtrl = ctrls[0]; - var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null; - if (!umbPropCtrl) { - //we cannot proceed, this validator will be disabled - return; - } - - var watcher = null; - - //Need to watch the value model for it to change, previously we had subscribed to - //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that - // doesn't specifically have a 2 way ng binding. This is required because when we - // have a server error we actually invalidate the form which means it cannot be - // resubmitted. So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - function startWatch() { - //if there's not already a watch - if (!watcher) { - watcher = scope.$watch(function () { - return modelCtrl.$modelValue; - }, function (newValue, oldValue) { - - if (!newValue || angular.equals(newValue, oldValue)) { - return; - } - - if (modelCtrl.$invalid) { - modelCtrl.$setValidity('valServer', true); - stopWatch(); - } - }, true); - } - } - - function stopWatch() { - if (watcher) { - watcher(); - watcher = null; - } + }); } - - var currentProperty = umbPropCtrl.property; - - //default to 'value' if nothing is set - var fieldName = "value"; - if (attr.valServer) { - fieldName = scope.$eval(attr.valServer); - if (!fieldName) { - //eval returned nothing so just use the string - fieldName = attr.valServer; - } - } - - //subscribe to the server validation changes - serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) { - if (!isValid) { - modelCtrl.$setValidity('valServer', false); - //assign an error msg property to the current validator - modelCtrl.errorMsg = propertyErrors[0].errorMsg; - startWatch(); - } - else { - modelCtrl.$setValidity('valServer', true); - //reset the error message - modelCtrl.errorMsg = ""; - stopWatch(); + }; + } + angular.module('umbraco.directives.validation').directive('valServerField', valServerField); + /** +* @ngdoc directive +* @name umbraco.directives.directive:valSubView +* @restrict A +* @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data. +* In order for this directive to work, the valFormManager directive must be placed on the containing form. +**/ + (function () { + 'use strict'; + function valSubViewDirective() { + function link(scope, el, attr, ctrl) { + //if there are no containing form or valFormManager controllers, then we do nothing + if (!ctrl || !angular.isArray(ctrl) || ctrl.length !== 2 || !ctrl[0] || !ctrl[1]) { + return; } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - stopWatch(); - serverValidationManager.unsubscribe(currentProperty.alias, fieldName); - }); - } - }; -} -angular.module('umbraco.directives.validation').directive("valServer", valServer); -/** - * @ngdoc directive - * @name umbraco.directives.directive:valServerField - * @restrict A - * @description This directive is used to associate a content field (not user defined) with a server-side validation response - * so that the validators in angular are updated based on server-side feedback. - **/ -function valServerField(serverValidationManager) { - return { - require: 'ngModel', - restrict: "A", - link: function (scope, element, attr, ngModel) { - - var fieldName = null; - var eventBindings = []; - - attr.$observe("valServerField", function (newVal) { - if (newVal && fieldName === null) { - fieldName = newVal; - - //subscribe to the changed event of the view model. This is required because when we - // have a server error we actually invalidate the form which means it cannot be - // resubmitted. So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - eventBindings.push(scope.$watch(function() { - return ngModel.$modelValue; - }, function(newValue){ - if (ngModel.$invalid) { - ngModel.$setValidity('valServerField', true); - } - })); - - //subscribe to the server validation changes - serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { - if (!isValid) { - ngModel.$setValidity('valServerField', false); - //assign an error msg property to the current validator - ngModel.errorMsg = fieldErrors[0].errorMsg; - } - else { - ngModel.$setValidity('valServerField', true); - //reset the error message - ngModel.errorMsg = ""; + var valFormManager = ctrl[1]; + scope.subView.hasError = false; + //listen for form validation changes + valFormManager.onValidationStatusChanged(function (evt, args) { + if (!args.form.$valid) { + var subViewContent = el.find('.ng-invalid'); + if (subViewContent.length > 0) { + scope.subView.hasError = true; + } else { + scope.subView.hasError = false; } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - serverValidationManager.unsubscribe(null, fieldName); - }); - } - }); - - scope.$on('$destroy', function(){ - // unbind watchers - for(var e in eventBindings) { - eventBindings[e](); - } - }); - + } else { + scope.subView.hasError = false; + } + }); + } + var directive = { + require: [ + '?^form', + '?^valFormManager' + ], + restrict: 'A', + link: link + }; + return directive; } - }; -} -angular.module('umbraco.directives.validation').directive("valServerField", valServerField); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:valSubView -* @restrict A -* @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data. -* In order for this directive to work, the valFormManager directive must be placed on the containing form. -**/ -(function() { - 'use strict'; - - function valSubViewDirective() { - - function link(scope, el, attr, ctrl) { - - var valFormManager = ctrl[1]; - scope.subView.hasError = false; - - //listen for form validation changes - valFormManager.onValidationStatusChanged(function (evt, args) { - if (!args.form.$valid) { - - var subViewContent = el.find(".ng-invalid"); - - if (subViewContent.length > 0) { - scope.subView.hasError = true; - } else { - scope.subView.hasError = false; - } - - } - else { - scope.subView.hasError = false; - } - }); - - } - - var directive = { - require: ['^form', '^valFormManager'], - restrict: "A", - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('valSubView', valSubViewDirective); - -})(); - - -/** -* @ngdoc directive -* @name umbraco.directives.directive:valTab -* @restrict A -* @description Used to show validation warnings for a tab to indicate that the tab content has validations errors in its data. -* In order for this directive to work, the valFormManager directive must be placed on the containing form. + angular.module('umbraco.directives').directive('valSubView', valSubViewDirective); + }()); + /** +* @ngdoc directive +* @name umbraco.directives.directive:valTab +* @restrict A +* @description Used to show validation warnings for a tab to indicate that the tab content has validations errors in its data. +* In order for this directive to work, the valFormManager directive must be placed on the containing form. **/ -function valTab() { - return { - require: ['^form', '^valFormManager'], - restrict: "A", - link: function (scope, element, attr, ctrs) { - - var valFormManager = ctrs[1]; - var tabId = "tab" + scope.tab.id; - scope.tabHasError = false; - - //listen for form validation changes - valFormManager.onValidationStatusChanged(function (evt, args) { - if (!args.form.$valid) { - var tabContent = element.closest(".umb-panel").find("#" + tabId); - //check if the validation messages are contained inside of this tabs - if (tabContent.find(".ng-invalid").length > 0) { - scope.tabHasError = true; + function valTab() { + return { + require: [ + '^form', + '^valFormManager' + ], + restrict: 'A', + link: function (scope, element, attr, ctrs) { + var valFormManager = ctrs[1]; + var tabId = 'tab' + scope.tab.id; + scope.tabHasError = false; + //listen for form validation changes + valFormManager.onValidationStatusChanged(function (evt, args) { + if (!args.form.$valid) { + var tabContent = element.closest('.umb-panel').find('#' + tabId); + //check if the validation messages are contained inside of this tabs + if (tabContent.find('.ng-invalid').length > 0) { + scope.tabHasError = true; + } else { + scope.tabHasError = false; + } } else { scope.tabHasError = false; } - } - else { - scope.tabHasError = false; - } - }); - - } - }; -} -angular.module('umbraco.directives.validation').directive("valTab", valTab); -function valToggleMsg(serverValidationManager) { - return { - require: "^form", - restrict: "A", - - /** - Our directive requries a reference to a form controller which gets passed in to this parameter - */ - link: function (scope, element, attr, formCtrl) { - - if (!attr.valToggleMsg){ - throw "valToggleMsg requires that a reference to a validator is specified"; - } - if (!attr.valMsgFor){ - throw "valToggleMsg requires that the attribute valMsgFor exists on the element"; - } - if (!formCtrl[attr.valMsgFor]) { - throw "valToggleMsg cannot find field " + attr.valMsgFor + " on form " + formCtrl.$name; + }); } - - //if there's any remaining errors in the server validation service then we should show them. - var showValidation = serverValidationManager.items.length > 0; - var hasCustomMsg = element.contents().length > 0; - - //add a watch to the validator for the value (i.e. myForm.value.$error.required ) - scope.$watch(function () { - //sometimes if a dialog closes in the middle of digest we can get null references here - - return (formCtrl && formCtrl[attr.valMsgFor]) ? formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] : null; - }, function () { - //sometimes if a dialog closes in the middle of digest we can get null references here - if ((formCtrl && formCtrl[attr.valMsgFor])) { - if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] && showValidation) { + }; + } + angular.module('umbraco.directives.validation').directive('valTab', valTab); + function valToggleMsg(serverValidationManager) { + return { + require: '^form', + restrict: 'A', + /** + Our directive requries a reference to a form controller which gets passed in to this parameter + */ + link: function (scope, element, attr, formCtrl) { + if (!attr.valToggleMsg) { + throw 'valToggleMsg requires that a reference to a validator is specified'; + } + if (!attr.valMsgFor) { + throw 'valToggleMsg requires that the attribute valMsgFor exists on the element'; + } + if (!formCtrl[attr.valMsgFor]) { + throw 'valToggleMsg cannot find field ' + attr.valMsgFor + ' on form ' + formCtrl.$name; + } + //if there's any remaining errors in the server validation service then we should show them. + var showValidation = serverValidationManager.items.length > 0; + var hasCustomMsg = element.contents().length > 0; + //add a watch to the validator for the value (i.e. myForm.value.$error.required ) + scope.$watch(function () { + //sometimes if a dialog closes in the middle of digest we can get null references here + return formCtrl && formCtrl[attr.valMsgFor] ? formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] : null; + }, function () { + //sometimes if a dialog closes in the middle of digest we can get null references here + if (formCtrl && formCtrl[attr.valMsgFor]) { + if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] && showValidation) { + element.show(); + //display the error message if this element has no contents + if (!hasCustomMsg) { + element.html(formCtrl[attr.valMsgFor].errorMsg); + } + } else { + element.hide(); + } + } + }); + var unsubscribe = []; + //listen for the saving event (the result is a callback method which is called to unsubscribe) + unsubscribe.push(scope.$on('formSubmitting', function (ev, args) { + showValidation = true; + if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg]) { element.show(); //display the error message if this element has no contents if (!hasCustomMsg) { element.html(formCtrl[attr.valMsgFor].errorMsg); } - } - else { + } else { element.hide(); } - } - }); - - var unsubscribe = []; - - //listen for the saving event (the result is a callback method which is called to unsubscribe) - unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { - showValidation = true; - if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg]) { - element.show(); - //display the error message if this element has no contents - if (!hasCustomMsg) { - element.html(formCtrl[attr.valMsgFor].errorMsg); - } - } - else { + })); + //listen for the saved event (the result is a callback method which is called to unsubscribe) + unsubscribe.push(scope.$on('formSubmitted', function (ev, args) { + showValidation = false; element.hide(); - } - })); - - //listen for the saved event (the result is a callback method which is called to unsubscribe) - unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { - showValidation = false; - element.hide(); - })); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this directive is part of a modal, the listener still exists because the dom - // element might still be there even after the modal has been hidden. - element.bind('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - - } - }; -} - -/** -* @ngdoc directive -* @name umbraco.directives.directive:valToggleMsg -* @restrict A -* @element input -* @requires formController -* @description This directive will show/hide an error based on: is the value + the given validator invalid? AND, has the form been submitted ? + })); + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise if this directive is part of a modal, the listener still exists because the dom + // element might still be there even after the modal has been hidden. + element.bind('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + } + }; + } + /** +* @ngdoc directive +* @name umbraco.directives.directive:valToggleMsg +* @restrict A +* @element input +* @requires formController +* @description This directive will show/hide an error based on: is the value + the given validator invalid? AND, has the form been submitted ? **/ -angular.module('umbraco.directives.validation').directive("valToggleMsg", valToggleMsg); -angular.module('umbraco.directives.validation') -.directive('valTriggerChange', function($sniffer) { - return { - link : function(scope, elem, attrs) { - elem.bind('click', function(){ - $(attrs.valTriggerChange).trigger($sniffer.hasEvent('input') ? 'input' : 'change'); - }); - }, - priority : 1 - }; -}); - -})(); \ No newline at end of file + angular.module('umbraco.directives.validation').directive('valToggleMsg', valToggleMsg); + angular.module('umbraco.directives.validation').directive('valTriggerChange', function ($sniffer) { + return { + link: function (scope, elem, attrs) { + elem.bind('click', function () { + $(attrs.valTriggerChange).trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + }); + }, + priority: 1 + }; + }); +}()); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.filters.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.filters.js index a5c5730a..62f11a36 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.filters.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.filters.js @@ -1,53 +1,116 @@ -/*! umbraco - * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2017 Umbraco HQ; - * Licensed - */ - -(function() { - -angular.module('umbraco.filters', []); -angular.module("umbraco.filters") - .filter('compareArrays', function() { +(function () { + angular.module('umbraco.filters', []); + angular.module('umbraco.filters').filter('compareArrays', function () { return function inArray(array, compareArray, compareProperty) { - var result = []; - - angular.forEach(array, function(arrayItem){ - + angular.forEach(array, function (arrayItem) { var exists = false; - - angular.forEach(compareArray, function(compareItem){ - if( arrayItem[compareProperty] === compareItem[compareProperty]) { + angular.forEach(compareArray, function (compareItem) { + if (arrayItem[compareProperty] === compareItem[compareProperty]) { exists = true; } }); - - if(!exists) { + if (!exists) { result.push(arrayItem); } - }); - return result; - }; -}); - -angular.module("umbraco.filters").filter('timespan', function() { - return function(input) { - var sec_num = parseInt(input, 10); - var hours = Math.floor(sec_num / 3600); - var minutes = Math.floor((sec_num - (hours * 3600)) / 60); - var seconds = sec_num - (hours * 3600) - (minutes * 60); - - if (hours < 10) {hours = "0"+hours;} - if (minutes < 10) {minutes = "0"+minutes;} - if (seconds < 10) {seconds = "0"+seconds;} - var time = hours+':'+minutes+':'+seconds; - return time; + }); + // Filter to take a node id and grab it's name instead + // Usage: {{ pickerAlias | ncNodeName }} + // Cache for node names so we don't make a ton of requests + var ncNodeNameCache = { + id: '', + keys: {} }; - }); - - -})(); \ No newline at end of file + angular.module('umbraco.filters').filter('ncNodeName', function (editorState, entityResource) { + return function (input) { + // Check we have a value at all + if (input === '' || input.toString() === '0') { + return ''; + } + var currentNode = editorState.getCurrent(); + // Ensure a unique cache per editor instance + var key = 'ncNodeName_' + currentNode.key; + if (ncNodeNameCache.id !== key) { + ncNodeNameCache.id = key; + ncNodeNameCache.keys = {}; + } + // See if there is a value in the cache and use that + if (ncNodeNameCache.keys[input]) { + return ncNodeNameCache.keys[input]; + } + // No value, so go fetch one + // We'll put a temp value in the cache though so we don't + // make a load of requests while we wait for a response + ncNodeNameCache.keys[input] = 'Loading...'; + entityResource.getById(input, 'Document').then(function (ent) { + ncNodeNameCache.keys[input] = ent.name; + }); + // Return the current value for now + return ncNodeNameCache.keys[input]; + }; + }); + /** +* @ngdoc filter +* @name umbraco.filters.preserveNewLineInHtml +* @description +* Used when rendering a string as HTML (i.e. with ng-bind-html) to convert line-breaks to
    tags +**/ + angular.module('umbraco.filters').filter('preserveNewLineInHtml', function () { + return function (text) { + if (!text) { + return ''; + } + return text.replace(/\n/g, '
    '); + }; + }); + angular.module('umbraco.filters').filter('timespan', function () { + return function (input) { + var sec_num = parseInt(input, 10); + var hours = Math.floor(sec_num / 3600); + var minutes = Math.floor((sec_num - hours * 3600) / 60); + var seconds = sec_num - hours * 3600 - minutes * 60; + if (hours < 10) { + hours = '0' + hours; + } + if (minutes < 10) { + minutes = '0' + minutes; + } + if (seconds < 10) { + seconds = '0' + seconds; + } + var time = hours + ':' + minutes + ':' + seconds; + return time; + }; + }); + /** + * @ngdoc filter + * @name umbraco.filters.filter:umbWordLimit + * @namespace umbWordLimitFilter + * + * @description + * Limits the number of words in a string to the passed in value + */ + (function () { + 'use strict'; + function umbWordLimitFilter() { + return function (collection, property) { + if (!angular.isString(collection)) { + return collection; + } + if (angular.isUndefined(property)) { + return collection; + } + var newString = ''; + var array = []; + array = collection.split(' ', property); + array.length = property; + newString = array.join(' '); + return newString; + }; + } + angular.module('umbraco.filters').filter('umbWordLimit', umbWordLimitFilter); + }()); +}()); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.installer.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.installer.js index 5a6cc301..821dc159 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.installer.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.installer.js @@ -1,468 +1,412 @@ -/*! umbraco - * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2017 Umbraco HQ; - * Licensed - */ - -(function() { - - angular.module('umbraco.install', []); -angular.module("umbraco.install").controller("Umbraco.InstallerController", - function ($scope, installerService) { - - //TODO: Decouple the service from the controller - the controller should be responsible - // for the model (state) and the service should be responsible for helping the controller, - // the controller should be passing the model into it's methods for manipulation and not hold - // state. We should not be assigning properties from a service to a controller's scope. - // see: https://github.com/umbraco/Umbraco-CMS/commit/b86ef0d7ac83f699aee35d807f7f7ebb6dd0ed2c#commitcomment-5721204 - - $scope.stepIndex = 0; - //comment this out if you just want to see tips - installerService.init(); - - //uncomment this to see tips - //installerService.switchToFeedback(); - - $scope.installer = installerService.status; - - $scope.forward = function () { - installerService.forward(); - }; - - $scope.backward = function () { - installerService.backward(); - }; - - $scope.install = function () { - installerService.install(); - }; - - $scope.gotoStep = function (step) { - installerService.gotoNamedStep(step); - }; - - $scope.restart = function () { - installerService.gotoStep(0); - }; - }); - -//this ensure that we start with a clean slate on every install and upgrade -angular.module("umbraco.install").run(function ($templateCache) { - $templateCache.removeAll(); -}); -angular.module("umbraco.install").factory('installerService', function($rootScope, $q, $timeout, $http, $location, $log){ - - var _status = { - index: 0, - current: undefined, - steps: undefined, - loading: true, - progress: "100%" - }; - - var factTimer = undefined; - var _installerModel = { - installId: undefined, - instructions: { - } - }; - - //add to umbraco installer facts here - var facts = ['Umbraco helped millions of people watch a man jump from the edge of space', - 'Over 370 000 websites are currently powered by Umbraco', - "At least 2 people have named their cat 'Umbraco'", - 'On an average day, more than 1000 people download Umbraco', - 'umbraco.tv is the premier source of Umbraco video tutorials to get you started', - 'You can find the world\'s friendliest CMS community at our.umbraco.org', - 'You can become a certified Umbraco developer by attending one of the official courses', - 'Umbraco works really well on tablets', - 'You have 100% control over your markup and design when crafting a website in Umbraco', - 'Umbraco is the best of both worlds: 100% free and open source, and backed by a professional and profitable company', - "There's a pretty big chance, you've visited a website powered by Umbraco today", - "'Umbraco-spotting' is the game of spotting big brands running Umbraco", - "At least 4 people have the Umbraco logo tattooed on them", - "'Umbraco' is the danish name for an allen key", - "Umbraco has been around since 2005, that's a looong time in IT", - "More than 400 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", - "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", - "You can extend Umbraco without modifying the source code using either JavaScript or C#", - "Umbraco was installed in more than 165 countries in 2015" - ]; - - /** +(function () { + angular.module('umbraco.install', ['umbraco.directives']); + angular.module('umbraco.install').controller('Umbraco.InstallerController', function ($scope, installerService) { + //TODO: Decouple the service from the controller - the controller should be responsible + // for the model (state) and the service should be responsible for helping the controller, + // the controller should be passing the model into it's methods for manipulation and not hold + // state. We should not be assigning properties from a service to a controller's scope. + // see: https://github.com/umbraco/Umbraco-CMS/commit/b86ef0d7ac83f699aee35d807f7f7ebb6dd0ed2c#commitcomment-5721204 + $scope.stepIndex = 0; + //comment this out if you just want to see tips + installerService.init(); + //uncomment this to see tips + //installerService.switchToFeedback(); + $scope.installer = installerService.status; + $scope.forward = function () { + installerService.forward(); + }; + $scope.backward = function () { + installerService.backward(); + }; + $scope.install = function () { + installerService.install(); + }; + $scope.gotoStep = function (step) { + installerService.gotoNamedStep(step); + }; + $scope.restart = function () { + installerService.gotoStep(0); + }; + }); + //this ensure that we start with a clean slate on every install and upgrade + angular.module('umbraco.install').run(function ($templateCache) { + $templateCache.removeAll(); + }); + angular.module('umbraco.install').factory('installerService', function ($rootScope, $q, $timeout, $http, $location, $log) { + var _status = { + index: 0, + current: undefined, + steps: undefined, + loading: true, + progress: '100%' + }; + var factTimer = undefined; + var _installerModel = { + installId: undefined, + instructions: {} + }; + //add to umbraco installer facts here + var facts = [ + 'Umbraco helped millions of people watch a man jump from the edge of space', + 'Over 440 000 websites are currently powered by Umbraco', + 'At least 2 people have named their cat \'Umbraco\'', + 'On an average day, more than 1000 people download Umbraco', + 'umbraco.tv is the premier source of Umbraco video tutorials to get you started', + 'You can find the world\'s friendliest CMS community at our.umbraco.org', + 'You can become a certified Umbraco developer by attending one of the official courses', + 'Umbraco works really well on tablets', + 'You have 100% control over your markup and design when crafting a website in Umbraco', + 'Umbraco is the best of both worlds: 100% free and open source, and backed by a professional and profitable company', + 'There\'s a pretty big chance, you\'ve visited a website powered by Umbraco today', + '\'Umbraco-spotting\' is the game of spotting big brands running Umbraco', + 'At least 4 people have the Umbraco logo tattooed on them', + '\'Umbraco\' is the danish name for an allen key', + 'Umbraco has been around since 2005, that\'s a looong time in IT', + 'More than 600 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden', + 'While you are installing Umbraco someone else on the other side of the planet is probably doing it too', + 'You can extend Umbraco without modifying the source code using either JavaScript or C#', + 'Umbraco has been installed in more than 198 countries' + ]; + /** Returns the description for the step at a given index based on the order of the serverOrder of steps Since they don't execute on the server in the order that they are displayed in the UI. */ - function getDescriptionForStepAtIndex(steps, index) { - var sorted = _.sortBy(steps, "serverOrder"); - if (sorted[index]) { - return sorted[index].description; - } - return null; - } - /* Returns the description for the given step name */ - function getDescriptionForStepName(steps, name) { - var found = _.find(steps, function(i) { - return i.name == name; - }); - return (found) ? found.description : null; - } - - //calculates the offset of the progressbar on the installer - function calculateProgress(steps, next) { - var sorted = _.sortBy(steps, "serverOrder"); - - var pct = "100%"; - for (var i = sorted.length - 1; i >= 0; i--) { - if(sorted[i].name == next){ - pct = Math.floor((i+1) / steps.length * 100) + "%"; - break; - } - } - return pct; - } - - //helpful defaults for the view loading - function resolveView(view){ - - if(view.indexOf(".html") < 0){ - view = view + ".html"; - } - if(view.indexOf("/") < 0){ - view = "views/install/" + view; - } - - return view; - } - - /** Have put this here because we are not referencing our other modules */ - function safeApply (scope, fn) { - if (scope.$$phase || scope.$root.$$phase) { - if (angular.isFunction(fn)) { - fn(); - } - } - else { - if (angular.isFunction(fn)) { - scope.$apply(fn); - } - else { - scope.$apply(); - } - } - } - - var service = { - - status : _status, - //loads the needed steps and sets the intial state - init : function(){ - service.status.loading = true; - if(!_status.all){ - service.getSteps().then(function(response){ - service.status.steps = response.data.steps; - service.status.index = 0; - _installerModel.installId = response.data.installId; - service.findNextStep(); - - $timeout(function(){ - service.status.loading = false; - service.status.configuring = true; - }, 2000); - }); - } - }, - - //loads available packages from our.umbraco.org - getPackages : function(){ - return $http.get(Umbraco.Sys.ServerVariables.installApiBaseUrl + "GetPackages"); - }, - - getSteps : function(){ - return $http.get(Umbraco.Sys.ServerVariables.installApiBaseUrl + "GetSetup"); - }, - - gotoStep : function(index){ - var step = service.status.steps[index]; - step.view = resolveView(step.view); - - if(!step.model){ - step.model = {}; - } - - service.status.index = index; - service.status.current = step; - service.retrieveCurrentStep(); - }, - - gotoNamedStep : function(stepName){ - var step = _.find(service.status.steps, function(s, index){ - if (s.view && s.name === stepName) { - service.status.index = index; - return true; - } - return false; - }); - - step.view = resolveView(step.view); - if(!step.model){ - step.model = {}; - } - service.retrieveCurrentStep(); - service.status.current = step; - }, - - /** - Finds the next step containing a view. If one is found it stores it as the current step - and retreives the step information and returns it, otherwise returns null . - */ - findNextStep : function(){ - var step = _.find(service.status.steps, function(s, index){ - if(s.view && index >= service.status.index){ - service.status.index = index; - return true; - } - return false; - }); - - if (step) { - if (step.view.indexOf(".html") < 0) { - step.view = step.view + ".html"; + function getDescriptionForStepAtIndex(steps, index) { + var sorted = _.sortBy(steps, 'serverOrder'); + if (sorted[index]) { + return sorted[index].description; + } + return null; + } + /* Returns the description for the given step name */ + function getDescriptionForStepName(steps, name) { + var found = _.find(steps, function (i) { + return i.name == name; + }); + return found ? found.description : null; + } + //calculates the offset of the progressbar on the installer + function calculateProgress(steps, next) { + var sorted = _.sortBy(steps, 'serverOrder'); + var pct = '100%'; + for (var i = sorted.length - 1; i >= 0; i--) { + if (sorted[i].name == next) { + pct = Math.floor((i + 1) / steps.length * 100) + '%'; + break; + } + } + return pct; + } + //helpful defaults for the view loading + function resolveView(view) { + if (view.indexOf('.html') < 0) { + view = view + '.html'; + } + if (view.indexOf('/') < 0) { + view = 'views/install/' + view; + } + return view; + } + /** Have put this here because we are not referencing our other modules */ + function safeApply(scope, fn) { + if (scope.$$phase || scope.$root.$$phase) { + if (angular.isFunction(fn)) { + fn(); } - - if (step.view.indexOf("/") < 0) { - step.view = "views/install/" + step.view; + } else { + if (angular.isFunction(fn)) { + scope.$apply(fn); + } else { + scope.$apply(); } - + } + } + var service = { + status: _status, + //loads the needed steps and sets the intial state + init: function () { + service.status.loading = true; + if (!_status.all) { + service.getSteps().then(function (response) { + service.status.steps = response.data.steps; + service.status.index = 0; + _installerModel.installId = response.data.installId; + service.findNextStep(); + $timeout(function () { + service.status.loading = false; + service.status.configuring = true; + }, 2000); + }); + } + }, + //loads available packages from our.umbraco.org + getPackages: function () { + return $http.get(Umbraco.Sys.ServerVariables.installApiBaseUrl + 'GetPackages'); + }, + getSteps: function () { + return $http.get(Umbraco.Sys.ServerVariables.installApiBaseUrl + 'GetSetup'); + }, + gotoStep: function (index) { + var step = service.status.steps[index]; + step.view = resolveView(step.view); if (!step.model) { step.model = {}; } - + service.status.index = index; service.status.current = step; service.retrieveCurrentStep(); - - //returns the next found step - return step; + }, + gotoNamedStep: function (stepName) { + var step = _.find(service.status.steps, function (s, index) { + if (s.view && s.name === stepName) { + service.status.index = index; + return true; + } + return false; + }); + step.view = resolveView(step.view); + if (!step.model) { + step.model = {}; + } + service.retrieveCurrentStep(); + service.status.current = step; + }, + /** + Finds the next step containing a view. If one is found it stores it as the current step + and retreives the step information and returns it, otherwise returns null . + */ + findNextStep: function () { + var step = _.find(service.status.steps, function (s, index) { + if (s.view && index >= service.status.index) { + service.status.index = index; + return true; + } + return false; + }); + if (step) { + if (step.view.indexOf('.html') < 0) { + step.view = step.view + '.html'; + } + if (step.view.indexOf('/') < 0) { + step.view = 'views/install/' + step.view; + } + if (!step.model) { + step.model = {}; + } + service.status.current = step; + service.retrieveCurrentStep(); + //returns the next found step + return step; + } else { + //there are no more steps found containing a view so return null + return null; + } + }, + storeCurrentStep: function () { + _installerModel.instructions[service.status.current.name] = service.status.current.model; + }, + retrieveCurrentStep: function () { + if (_installerModel.instructions[service.status.current.name]) { + service.status.current.model = _installerModel.instructions[service.status.current.name]; + } + }, + /** Moves the installer forward to the next view, if there are not more views than the installation will commence */ + forward: function () { + service.storeCurrentStep(); + service.status.index++; + var found = service.findNextStep(); + if (!found) { + //no more steps were found so start the installation process + service.install(); + } + }, + backwards: function () { + service.storeCurrentStep(); + service.gotoStep(service.status.index--); + }, + install: function () { + service.storeCurrentStep(); + service.switchToFeedback(); + service.status.feedback = getDescriptionForStepAtIndex(service.status.steps, 0); + service.status.progress = 0; + function processInstallStep() { + $http.post(Umbraco.Sys.ServerVariables.installApiBaseUrl + 'PostPerformInstall', _installerModel).success(function (data, status, headers, config) { + if (!data.complete) { + //progress feedback + service.status.progress = calculateProgress(service.status.steps, data.nextStep); + if (data.view) { + //set the current view and model to whatever the process returns, the view is responsible for retriggering install(); + var v = resolveView(data.view); + service.status.current = { + view: v, + model: data.model + }; + //turn off loading bar and feedback + service.switchToConfiguration(); + } else { + var desc = getDescriptionForStepName(service.status.steps, data.nextStep); + if (desc) { + service.status.feedback = desc; + } + processInstallStep(); + } + } else { + service.complete(); + } + }).error(function (data, status, headers, config) { + //need to handle 500's separately, this will happen if something goes wrong outside + // of the installer (like app startup events or something) and these will get returned as text/html + // not as json. If this happens we can't actually load in external views since they will YSOD as well! + // so we need to display this in our own internal way + if (status >= 500 && status < 600) { + service.status.current = { + view: 'ysod', + model: null + }; + var ysod = data; + //we need to manually write the html to the iframe - the html contains full html markup + $timeout(function () { + document.getElementById('ysod').contentDocument.write(ysod); + }, 500); + } else { + //this is where we handle installer error + var v = data.view ? resolveView(data.view) : resolveView('error'); + var model = data.model ? data.model : data; + service.status.current = { + view: v, + model: model + }; + } + service.switchToConfiguration(); + }); + } + processInstallStep(); + }, + randomFact: function () { + safeApply($rootScope, function () { + service.status.fact = facts[_.random(facts.length - 1)]; + }); + }, + switchToFeedback: function () { + service.status.current = undefined; + service.status.loading = true; + service.status.configuring = false; + //initial fact + service.randomFact(); + //timed facts + factTimer = window.setInterval(function () { + service.randomFact(); + }, 6000); + }, + switchToConfiguration: function () { + service.status.loading = false; + service.status.configuring = true; + service.status.feedback = undefined; + service.status.fact = undefined; + if (factTimer) { + clearInterval(factTimer); + } + }, + complete: function () { + service.status.progress = '100%'; + service.status.done = true; + service.status.feedback = 'Redirecting you to Umbraco, please wait'; + service.status.loading = false; + if (factTimer) { + clearInterval(factTimer); + } + $timeout(function () { + window.location.href = Umbraco.Sys.ServerVariables.umbracoBaseUrl; + }, 1500); } - else { - //there are no more steps found containing a view so return null - return null; + }; + return service; + }); + angular.module('umbraco.install').controller('Umbraco.Installer.DataBaseController', function ($scope, $http, installerService) { + $scope.checking = false; + $scope.invalidDbDns = false; + $scope.dbs = [ + { + name: 'Microsoft SQL Server Compact (SQL CE)', + id: 0 + }, + { + name: 'Microsoft SQL Server', + id: 1 + }, + { + name: 'Microsoft SQL Azure', + id: 3 + }, + { + name: 'MySQL', + id: 2 + }, + { + name: 'Custom connection string', + id: -1 } - }, - - storeCurrentStep : function(){ - _installerModel.instructions[service.status.current.name] = service.status.current.model; - }, - - retrieveCurrentStep : function(){ - if(_installerModel.instructions[service.status.current.name]){ - service.status.current.model = _installerModel.instructions[service.status.current.name]; - } - }, - - /** Moves the installer forward to the next view, if there are not more views than the installation will commence */ - forward : function(){ - service.storeCurrentStep(); - service.status.index++; - var found = service.findNextStep(); - if (!found) { - //no more steps were found so start the installation process - service.install(); + ]; + if (installerService.status.current.model.dbType === undefined) { + installerService.status.current.model.dbType = 0; + } + $scope.validateAndForward = function () { + if (!$scope.checking && this.myForm.$valid) { + $scope.checking = true; + $scope.invalidDbDns = false; + var model = installerService.status.current.model; + $http.post(Umbraco.Sys.ServerVariables.installApiBaseUrl + 'PostValidateDatabaseConnection', model).then(function (response) { + if (response.data === 'true') { + installerService.forward(); + } else { + $scope.invalidDbDns = true; + } + $scope.checking = false; + }, function () { + $scope.invalidDbDns = true; + $scope.checking = false; + }); } - }, - - backwards : function(){ - service.storeCurrentStep(); - service.gotoStep(service.status.index--); - }, - - install : function(){ - service.storeCurrentStep(); - service.switchToFeedback(); - - service.status.feedback = getDescriptionForStepAtIndex(service.status.steps, 0); - service.status.progress = 0; - - function processInstallStep() { - - $http.post(Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostPerformInstall", _installerModel) - .success(function(data, status, headers, config) { - if (!data.complete) { - - //progress feedback - service.status.progress = calculateProgress(service.status.steps, data.nextStep); - - if (data.view) { - //set the current view and model to whatever the process returns, the view is responsible for retriggering install(); - var v = resolveView(data.view); - service.status.current = { view: v, model: data.model }; - - //turn off loading bar and feedback - service.switchToConfiguration(); - } - else { - var desc = getDescriptionForStepName(service.status.steps, data.nextStep); - if (desc) { - service.status.feedback = desc; - } - processInstallStep(); - } - } - else { - service.complete(); - } - }).error(function(data, status, headers, config) { - //need to handle 500's separately, this will happen if something goes wrong outside - // of the installer (like app startup events or something) and these will get returned as text/html - // not as json. If this happens we can't actually load in external views since they will YSOD as well! - // so we need to display this in our own internal way - - if (status >= 500 && status < 600) { - service.status.current = { view: "ysod", model: null }; - var ysod = data; - //we need to manually write the html to the iframe - the html contains full html markup - $timeout(function () { - document.getElementById('ysod').contentDocument.write(ysod); - }, 500); - } - else { - //this is where we handle installer error - var v = data.view ? resolveView(data.view) : resolveView("error"); - var model = data.model ? data.model : data; - service.status.current = { view: v, model: model }; - } - - service.switchToConfiguration(); - - }); - } - processInstallStep(); - }, - - randomFact: function () { - safeApply($rootScope, function() { - service.status.fact = facts[_.random(facts.length - 1)]; - }); - }, - - switchToFeedback : function(){ - service.status.current = undefined; - service.status.loading = true; - service.status.configuring = false; - - //initial fact - service.randomFact(); - - //timed facts - factTimer = window.setInterval(function(){ - service.randomFact(); - },6000); - }, - - switchToConfiguration : function(){ - service.status.loading = false; - service.status.configuring = true; - service.status.feedback = undefined; - service.status.fact = undefined; - - if(factTimer){ - clearInterval(factTimer); - } - }, - - complete : function(){ - - service.status.progress = "100%"; - service.status.done = true; - service.status.feedback = "Redirecting you to Umbraco, please wait"; - service.status.loading = false; - - if(factTimer){ - clearInterval(factTimer); - } - - $timeout(function(){ - window.location.href = Umbraco.Sys.ServerVariables.umbracoBaseUrl; - }, 1500); - } - }; - - return service; -}); - -angular.module("umbraco.install").controller("Umbraco.Installer.DataBaseController", function($scope, $http, installerService){ - - $scope.checking = false; - $scope.dbs = [ - {name: 'Microsoft SQL Server Compact (SQL CE)', id: 0}, - {name: 'Microsoft SQL Server', id: 1}, - { name: 'Microsoft SQL Azure', id: 3 }, - { name: 'MySQL', id: 2 }, - {name: 'Custom connection string', id: -1}]; - - if(installerService.status.current.model.dbType === undefined){ - installerService.status.current.model.dbType = 0; - } - - $scope.validateAndForward = function(){ - if(!$scope.checking && this.myForm.$valid){ - $scope.checking = true; - var model = installerService.status.current.model; - - $http.post(Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostValidateDatabaseConnection", - model).then(function(response){ - - if(response.data === "true"){ - installerService.forward(); - }else{ - $scope.invalidDbDns = true; - } - - $scope.checking = false; - }, function(){ - $scope.invalidDbDns = true; - $scope.checking = false; - }); - } - }; -}); -angular.module("umbraco.install").controller("Umbraco.Installer.PackagesController", function ($scope, installerService) { - - installerService.getPackages().then(function (response) { - $scope.packages = response.data; + }; }); - - $scope.setPackageAndContinue = function (pckId) { - installerService.status.current.model = pckId; - installerService.forward(); - }; - -}); -angular.module("umbraco.install").controller("Umbraco.Install.UserController", function($scope, installerService) { - - $scope.passwordPattern = /.*/; - $scope.installer.current.model.subscribeToNewsLetter = true; - - if ($scope.installer.current.model.minNonAlphaNumericLength > 0) { - var exp = ""; - for (var i = 0; i < $scope.installer.current.model.minNonAlphaNumericLength; i++) { - exp += ".*[\\W].*"; + angular.module('umbraco.install').controller('Umbraco.Installer.MachineKeyController', function ($scope, installerService) { + $scope.continue = function () { + installerService.status.current.model = true; + installerService.forward(); + }; + $scope.ignoreKey = function () { + installerService.status.current.model = false; + installerService.forward(); + }; + }); + angular.module('umbraco.install').controller('Umbraco.Installer.PackagesController', function ($scope, installerService) { + installerService.getPackages().then(function (response) { + $scope.packages = response.data; + }); + $scope.setPackageAndContinue = function (pckId) { + installerService.status.current.model = pckId; + installerService.forward(); + }; + }); + angular.module('umbraco.install').controller('Umbraco.Install.UserController', function ($scope, installerService) { + $scope.passwordPattern = /.*/; + $scope.installer.current.model.subscribeToNewsLetter = true; + if ($scope.installer.current.model.minNonAlphaNumericLength > 0) { + var exp = ''; + for (var i = 0; i < $scope.installer.current.model.minNonAlphaNumericLength; i++) { + exp += '.*[\\W].*'; + } + //replace duplicates + exp = exp.replace('.*.*', '.*'); + $scope.passwordPattern = new RegExp(exp); } - //replace duplicates - exp = exp.replace(".*.*", ".*"); - $scope.passwordPattern = new RegExp(exp); - } - - $scope.validateAndInstall = function(){ - installerService.install(); - }; - - $scope.validateAndForward = function(){ - if(this.myForm.$valid){ - installerService.forward(); - } - }; - -}); - -})(); \ No newline at end of file + $scope.validateAndInstall = function () { + installerService.install(); + }; + $scope.validateAndForward = function () { + if (this.myForm.$valid) { + installerService.forward(); + } + }; + }); +}()); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.resources.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.resources.js index 3be2a25d..607b232e 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.resources.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.resources.js @@ -1,13 +1,6 @@ -/*! umbraco - * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2017 Umbraco HQ; - * Licensed - */ - -(function() { - -angular.module("umbraco.resources", []); -/** +(function () { + angular.module('umbraco.resources', []); + /** * @ngdoc service * @name umbraco.resources.authResource * @description @@ -18,355 +11,257 @@ angular.module("umbraco.resources", []); * @requires umbRequestHelper * @requires angularHelper */ -function authResource($q, $http, umbRequestHelper, angularHelper) { - - return { - - get2FAProviders: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "Get2FAProviders")), - 'Could not retrive two factor provider info'); - }, - - send2FACode: function (provider) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostSend2FACode"), - angular.toJson(provider)), - 'Could not send code'); - }, - - verify2FACode: function (provider, code) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostVerify2FACode"), - { - code: code, - provider: provider - }), - 'Could not verify code'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#performLogin - * @methodOf umbraco.resources.authResource - * - * @description - * Logs the Umbraco backoffice user in if the credentials are good - * - * ##usage - *
    -         * authResource.performLogin(login, password)
    -         *    .then(function(data) {
    -         *        //Do stuff for login...
    -         *    });
    -         * 
    - * @param {string} login Username of backoffice user - * @param {string} password Password of backoffice user - * @returns {Promise} resourcePromise object - * - */ - performLogin: function (username, password) { - - if (!username || !password) { - return angularHelper.rejectedPromise({ - errorMsg: 'Username or password cannot be empty' - }); - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostLogin"), { - username: username, - password: password - }), - 'Login failed for user ' + username); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#performRequestPasswordReset - * @methodOf umbraco.resources.authResource - * - * @description - * Checks to see if the provided email address is a valid user account and sends a link - * to allow them to reset their password - * - * ##usage - *
    -         * authResource.performRequestPasswordReset(email)
    -         *    .then(function(data) {
    -         *        //Do stuff for password reset request...
    -         *    });
    -         * 
    - * @param {string} email Email address of backoffice user - * @returns {Promise} resourcePromise object - * - */ - performRequestPasswordReset: function (email) { - - if (!email) { - return angularHelper.rejectedPromise({ - errorMsg: 'Email address cannot be empty' - }); - } - - //TODO: This validation shouldn't really be done here, the validation on the login dialog - // is pretty hacky which is why this is here, ideally validation on the login dialog would - // be done properly. - var emailRegex = /\S+@\S+\.\S+/; - if (!emailRegex.test(email)) { - return angularHelper.rejectedPromise({ - errorMsg: 'Email address is not valid' - }); - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostRequestPasswordReset"), { - email: email - }), - 'Request password reset failed for email ' + email); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#performValidatePasswordResetCode - * @methodOf umbraco.resources.authResource - * - * @description - * Checks to see if the provided password reset code is valid - * - * ##usage - *
    -         * authResource.performValidatePasswordResetCode(resetCode)
    -         *    .then(function(data) {
    -         *        //Allow reset of password
    -         *    });
    -         * 
    - * @param {integer} userId User Id - * @param {string} resetCode Password reset code - * @returns {Promise} resourcePromise object - * - */ - performValidatePasswordResetCode: function (userId, resetCode) { - - if (!userId) { - return angularHelper.rejectedPromise({ - errorMsg: 'User Id cannot be empty' - }); - } - - if (!resetCode) { - return angularHelper.rejectedPromise({ - errorMsg: 'Reset code cannot be empty' - }); - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostValidatePasswordResetCode"), - { - userId: userId, - resetCode: resetCode - }), - 'Password reset code validation failed for userId ' + userId + ', code' + resetCode); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#performSetPassword - * @methodOf umbraco.resources.authResource - * - * @description - * Checks to see if the provided password reset code is valid and sets the user's password - * - * ##usage - *
    -         * authResource.performSetPassword(userId, password, confirmPassword, resetCode)
    -         *    .then(function(data) {
    -         *        //Password set
    -         *    });
    -         * 
    - * @param {integer} userId User Id - * @param {string} password New password - * @param {string} confirmPassword Confirmation of new password - * @param {string} resetCode Password reset code - * @returns {Promise} resourcePromise object - * - */ - performSetPassword: function (userId, password, confirmPassword, resetCode) { - - if (userId === undefined || userId === null) { - return angularHelper.rejectedPromise({ - errorMsg: 'User Id cannot be empty' - }); - } - - if (!password) { - return angularHelper.rejectedPromise({ - errorMsg: 'Password cannot be empty' - }); - } - - if (password !== confirmPassword) { - return angularHelper.rejectedPromise({ - errorMsg: 'Password and confirmation do not match' - }); - } - - if (!resetCode) { - return angularHelper.rejectedPromise({ - errorMsg: 'Reset code cannot be empty' - }); - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostSetPassword"), - { - userId: userId, - password: password, - resetCode: resetCode - }), - 'Password reset code validation failed for userId ' + userId); - }, - - unlinkLogin: function (loginProvider, providerKey) { - if (!loginProvider || !providerKey) { - return angularHelper.rejectedPromise({ - errorMsg: 'loginProvider or providerKey cannot be empty' - }); - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostUnLinkLogin"), { - loginProvider: loginProvider, - providerKey: providerKey - }), - 'Unlinking login provider failed'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#performLogout - * @methodOf umbraco.resources.authResource - * - * @description - * Logs out the Umbraco backoffice user - * - * ##usage - *
    -         * authResource.performLogout()
    -         *    .then(function(data) {
    -         *        //Do stuff for logging out...
    -         *    });
    -         * 
    - * @returns {Promise} resourcePromise object - * - */ - performLogout: function() { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostLogout"))); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#getCurrentUser - * @methodOf umbraco.resources.authResource - * - * @description - * Sends a request to the server to get the current user details, will return a 401 if the user is not logged in - * - * ##usage - *
    -         * authResource.getCurrentUser()
    -         *    .then(function(data) {
    -         *        //Do stuff for fetching the current logged in Umbraco backoffice user
    -         *    });
    -         * 
    - * @returns {Promise} resourcePromise object - * - */ - getCurrentUser: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "GetCurrentUser")), - 'Server call failed for getting current user'); - }, - - getCurrentUserLinkedLogins: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "GetCurrentUserLinkedLogins")), - 'Server call failed for getting current users linked logins'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#isAuthenticated - * @methodOf umbraco.resources.authResource - * - * @description - * Checks if the user is logged in or not - does not return 401 or 403 - * - * ##usage - *
    -         * authResource.isAuthenticated()
    -         *    .then(function(data) {
    -         *        //Do stuff to check if user is authenticated
    -         *    });
    -         * 
    - * @returns {Promise} resourcePromise object - * - */ - isAuthenticated: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "IsAuthenticated")), - { + function authResource($q, $http, umbRequestHelper, angularHelper) { + return { + get2FAProviders: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'Get2FAProviders')), 'Could not retrive two factor provider info'); + }, + send2FACode: function (provider) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostSend2FACode'), angular.toJson(provider)), 'Could not send code'); + }, + verify2FACode: function (provider, code) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostVerify2FACode'), { + code: code, + provider: provider + }), 'Could not verify code'); + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#performLogin + * @methodOf umbraco.resources.authResource + * + * @description + * Logs the Umbraco backoffice user in if the credentials are good + * + * ##usage + *
    +     * authResource.performLogin(login, password)
    +     *    .then(function(data) {
    +     *        //Do stuff for login...
    +     *    });
    +     * 
    + * @param {string} login Username of backoffice user + * @param {string} password Password of backoffice user + * @returns {Promise} resourcePromise object + * + */ + performLogin: function (username, password) { + if (!username || !password) { + return angularHelper.rejectedPromise({ errorMsg: 'Username or password cannot be empty' }); + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostLogin'), { + username: username, + password: password + }), 'Login failed for user ' + username); + }, + /** + * There are not parameters for this since when the user has clicked on their invite email they will be partially + * logged in (but they will not be approved) so we need to use this method to verify the non approved logged in user's details. + * Using the getCurrentUser will not work since that only works for approved users + * @returns {} + */ + getCurrentInvitedUser: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'GetCurrentInvitedUser')), 'Failed to verify invite'); + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#performRequestPasswordReset + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided email address is a valid user account and sends a link + * to allow them to reset their password + * + * ##usage + *
    +     * authResource.performRequestPasswordReset(email)
    +     *    .then(function(data) {
    +     *        //Do stuff for password reset request...
    +     *    });
    +     * 
    + * @param {string} email Email address of backoffice user + * @returns {Promise} resourcePromise object + * + */ + performRequestPasswordReset: function (email) { + if (!email) { + return angularHelper.rejectedPromise({ errorMsg: 'Email address cannot be empty' }); + } + //TODO: This validation shouldn't really be done here, the validation on the login dialog + // is pretty hacky which is why this is here, ideally validation on the login dialog would + // be done properly. + var emailRegex = /\S+@\S+\.\S+/; + if (!emailRegex.test(email)) { + return angularHelper.rejectedPromise({ errorMsg: 'Email address is not valid' }); + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostRequestPasswordReset'), { email: email }), 'Request password reset failed for email ' + email); + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#performValidatePasswordResetCode + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided password reset code is valid + * + * ##usage + *
    +     * authResource.performValidatePasswordResetCode(resetCode)
    +     *    .then(function(data) {
    +     *        //Allow reset of password
    +     *    });
    +     * 
    + * @param {integer} userId User Id + * @param {string} resetCode Password reset code + * @returns {Promise} resourcePromise object + * + */ + performValidatePasswordResetCode: function (userId, resetCode) { + if (!userId) { + return angularHelper.rejectedPromise({ errorMsg: 'User Id cannot be empty' }); + } + if (!resetCode) { + return angularHelper.rejectedPromise({ errorMsg: 'Reset code cannot be empty' }); + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostValidatePasswordResetCode'), { + userId: userId, + resetCode: resetCode + }), 'Password reset code validation failed for userId ' + userId + ', code' + resetCode); + }, + /** + * @ngdoc method + * @name umbraco.resources.currentUserResource#getMembershipProviderConfig + * @methodOf umbraco.resources.currentUserResource + * + * @description + * Gets the configuration of the user membership provider which is used to configure the change password form + */ + getMembershipProviderConfig: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'GetMembershipProviderConfig')), 'Failed to retrieve membership provider config'); + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#performSetPassword + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided password reset code is valid and sets the user's password + * + * ##usage + *
    +     * authResource.performSetPassword(userId, password, confirmPassword, resetCode)
    +     *    .then(function(data) {
    +     *        //Password set
    +     *    });
    +     * 
    + * @param {integer} userId User Id + * @param {string} password New password + * @param {string} confirmPassword Confirmation of new password + * @param {string} resetCode Password reset code + * @returns {Promise} resourcePromise object + * + */ + performSetPassword: function (userId, password, confirmPassword, resetCode) { + if (userId === undefined || userId === null) { + return angularHelper.rejectedPromise({ errorMsg: 'User Id cannot be empty' }); + } + if (!password) { + return angularHelper.rejectedPromise({ errorMsg: 'Password cannot be empty' }); + } + if (password !== confirmPassword) { + return angularHelper.rejectedPromise({ errorMsg: 'Password and confirmation do not match' }); + } + if (!resetCode) { + return angularHelper.rejectedPromise({ errorMsg: 'Reset code cannot be empty' }); + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostSetPassword'), { + userId: userId, + password: password, + resetCode: resetCode + }), 'Password reset code validation failed for userId ' + userId); + }, + unlinkLogin: function (loginProvider, providerKey) { + if (!loginProvider || !providerKey) { + return angularHelper.rejectedPromise({ errorMsg: 'loginProvider or providerKey cannot be empty' }); + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostUnLinkLogin'), { + loginProvider: loginProvider, + providerKey: providerKey + }), 'Unlinking login provider failed'); + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#performLogout + * @methodOf umbraco.resources.authResource + * + * @description + * Logs out the Umbraco backoffice user + * + * ##usage + *
    +     * authResource.performLogout()
    +     *    .then(function(data) {
    +     *        //Do stuff for logging out...
    +     *    });
    +     * 
    + * @returns {Promise} resourcePromise object + * + */ + performLogout: function () { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostLogout'))); + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#getCurrentUser + * @methodOf umbraco.resources.authResource + * + * @description + * Sends a request to the server to get the current user details, will return a 401 if the user is not logged in + * + * ##usage + *
    +     * authResource.getCurrentUser()
    +     *    .then(function(data) {
    +     *        //Do stuff for fetching the current logged in Umbraco backoffice user
    +     *    });
    +     * 
    + * @returns {Promise} resourcePromise object + * + */ + getCurrentUser: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'GetCurrentUser')), 'Server call failed for getting current user'); + }, + getCurrentUserLinkedLogins: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'GetCurrentUserLinkedLogins')), 'Server call failed for getting current users linked logins'); + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#isAuthenticated + * @methodOf umbraco.resources.authResource + * + * @description + * Checks if the user is logged in or not - does not return 401 or 403 + * + * ##usage + *
    +     * authResource.isAuthenticated()
    +     *    .then(function(data) {
    +     *        //Do stuff to check if user is authenticated
    +     *    });
    +     * 
    + * @returns {Promise} resourcePromise object + * + */ + isAuthenticated: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'IsAuthenticated')), { success: function (data, status, headers, config) { //if the response is false, they are not logged in so return a rejection - if (data === false || data === "false") { + if (data === false || data === 'false') { return $q.reject('User is not logged in'); } return data; }, - error: function (data, status, headers, config) { + error: function (data, status, headers, config) { return { errorMsg: 'Server call failed for checking authentication', data: data, @@ -374,51 +269,39 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { }; } }); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#getRemainingTimeoutSeconds - * @methodOf umbraco.resources.authResource - * - * @description - * Gets the user's remaining seconds before their login times out - * - * ##usage - *
    -         * authResource.getRemainingTimeoutSeconds()
    -         *    .then(function(data) {
    -         *        //Number of seconds is returned
    -         *    });
    -         * 
    - * @returns {Promise} resourcePromise object - * - */ - getRemainingTimeoutSeconds: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "GetRemainingTimeoutSeconds")), - 'Server call failed for checking remaining seconds'); - } - - }; -} - -angular.module('umbraco.resources').factory('authResource', authResource); - -/** + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#getRemainingTimeoutSeconds + * @methodOf umbraco.resources.authResource + * + * @description + * Gets the user's remaining seconds before their login times out + * + * ##usage + *
    +     * authResource.getRemainingTimeoutSeconds()
    +     *    .then(function(data) {
    +     *        //Number of seconds is returned
    +     *    });
    +     * 
    + * @returns {Promise} resourcePromise object + * + */ + getRemainingTimeoutSeconds: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'GetRemainingTimeoutSeconds')), 'Server call failed for checking remaining seconds'); + } + }; + } + angular.module('umbraco.resources').factory('authResource', authResource); + /** * @ngdoc service * @name umbraco.resources.codefileResource * @description Loads in data for files that contain code such as js scripts, partial views and partial view macros **/ -function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { - - return { - - /** + function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { + return { + /** * @ngdoc method * @name umbraco.resources.codefileResource#getByPath * @methodOf umbraco.resources.codefileResource @@ -446,18 +329,13 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getByPath: function (type, virtualpath) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "codeFileApiBaseUrl", - "GetByPath", - [{ type: type }, {virtualPath: virtualpath }])), - "Failed to retrieve data for " + type + " from virtual path " + virtualpath); - }, - - /** + getByPath: function (type, virtualpath) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('codeFileApiBaseUrl', 'GetByPath', [ + { type: type }, + { virtualPath: virtualpath } + ])), 'Failed to retrieve data for ' + type + ' from virtual path ' + virtualpath); + }, + /** * @ngdoc method * @name umbraco.resources.codefileResource#getByAlias * @methodOf umbraco.resources.codefileResource @@ -477,18 +355,10 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getByAlias: function (alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "templateApiBaseUrl", - "GetByAlias", - [{ alias: alias }])), - "Failed to retrieve data for template with alias: " + alias); - }, - - /** + getByAlias: function (alias) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('templateApiBaseUrl', 'GetByAlias', [{ alias: alias }])), 'Failed to retrieve data for template with alias: ' + alias); + }, + /** * @ngdoc method * @name umbraco.resources.codefileResource#deleteByPath * @methodOf umbraco.resources.codefileResource @@ -516,17 +386,13 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - deleteByPath: function (type, virtualpath) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "codeFileApiBaseUrl", - "Delete", - [{ type: type }, { virtualPath: virtualpath}])), - "Failed to delete item: " + virtualpath); - }, - - /** + deleteByPath: function (type, virtualpath) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('codeFileApiBaseUrl', 'Delete', [ + { type: type }, + { virtualPath: virtualpath } + ])), 'Failed to delete item: ' + virtualpath); + }, + /** * @ngdoc method * @name umbraco.resources.codefileResource#save * @methodOf umbraco.resources.codefileResource @@ -546,17 +412,10 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - save: function (codeFile) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "codeFileApiBaseUrl", - "PostSave"), - codeFile), - "Failed to save data for code file " + codeFile.virtualPath); - }, - - /** + save: function (codeFile) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('codeFileApiBaseUrl', 'PostSave'), codeFile), 'Failed to save data for code file ' + codeFile.virtualPath); + }, + /** * @ngdoc method * @name umbraco.resources.codefileResource#getSnippets * @methodOf umbraco.resources.codefileResource @@ -576,16 +435,10 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getSnippets: function (fileType) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "codeFileApiBaseUrl", - "GetSnippets?type=" + fileType )), - "Failed to get snippet for" + fileType); - }, - - /** + getSnippets: function (fileType) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('codeFileApiBaseUrl', 'GetSnippets?type=' + fileType)), 'Failed to get snippet for' + fileType); + }, + /** * @ngdoc method * @name umbraco.resources.codefileResource#getScaffold * @methodOf umbraco.resources.codefileResource @@ -608,23 +461,14 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - - getScaffold: function (type, id, snippetName) { - - var queryString = "?type=" + type + "&id=" + id; - if (snippetName) { - queryString += "&snippetName=" + snippetName; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "codeFileApiBaseUrl", - "GetScaffold" + queryString)), - "Failed to get scaffold for" + type); - }, - - /** + getScaffold: function (type, id, snippetName) { + var queryString = '?type=' + type + '&id=' + id; + if (snippetName) { + queryString += '&snippetName=' + snippetName; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('codeFileApiBaseUrl', 'GetScaffold' + queryString)), 'Failed to get scaffold for' + type); + }, + /** * @ngdoc method * @name umbraco.resources.codefileResource#createContainer * @methodOf umbraco.resources.codefileResource @@ -646,22 +490,17 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - - createContainer: function(type, parentId, name) { - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl( - "codeFileApiBaseUrl", - "PostCreateContainer", - { type: type, parentId: parentId, name: encodeURIComponent(name) })), - 'Failed to create a folder under parent id ' + parentId); - } - - }; -} - -angular.module("umbraco.resources").factory("codefileResource", codefileResource); - -/** + createContainer: function (type, parentId, name) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('codeFileApiBaseUrl', 'PostCreateContainer', { + type: type, + parentId: parentId, + name: encodeURIComponent(name) + })), 'Failed to create a folder under parent id ' + parentId); + } + }; + } + angular.module('umbraco.resources').factory('codefileResource', codefileResource); + /** * @ngdoc service * @name umbraco.resources.contentResource * @description Handles all transactions of content data @@ -685,36 +524,36 @@ angular.module("umbraco.resources").factory("codefileResource", codefileResource * }); * **/ - -function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { - - /** internal method process the saving of data and post processing the result */ - function saveContentItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatContentPostData(c, a); - } - }); - } - - return { - - getRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for content recycle bin'); - }, - - /** + function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { + /** internal method process the saving of data and post processing the result */ + function saveContentItem(content, action, files, restApiUrl) { + return umbRequestHelper.postSaveContent({ + restApiUrl: restApiUrl, + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatContentPostData(c, a); + } + }); + } + return { + savePermissions: function (saveModel) { + if (!saveModel) { + throw 'saveModel cannot be null'; + } + if (!saveModel.contentId) { + throw 'saveModel.contentId cannot be null'; + } + if (!saveModel.permissions) { + throw 'saveModel.permissions cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSaveUserGroupPermissions'), saveModel), 'Failed to save permissions'); + }, + getRecycleBin: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetRecycleBin')), 'Failed to retrieve data for content recycle bin'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#sort * @methodOf umbraco.resources.contentResource @@ -736,27 +575,22 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort content'); - }, - - /** + sort: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.sortedIds) { + throw 'args.sortedIds cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSort'), { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), 'Failed to sort content'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#move * @methodOf umbraco.resources.contentResource @@ -779,27 +613,22 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move content'); - }, - - /** + move: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.id) { + throw 'args.id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostMove'), { + parentId: args.parentId, + id: args.id + }), 'Failed to move content'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#copy * @methodOf umbraco.resources.contentResource @@ -823,24 +652,19 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - copy: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), - args), - 'Failed to copy content'); - }, - - /** + copy: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.id) { + throw 'args.id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostCopy'), args), 'Failed to copy content'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#unPublish * @methodOf umbraco.resources.contentResource @@ -861,20 +685,13 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - unPublish: function (id) { - if (!id) { - throw "id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostUnPublish", - [{ id: id }])), - 'Failed to publish content with id ' + id); - }, - /** + unPublish: function (id) { + if (!id) { + throw 'id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostUnPublish', [{ id: id }])), 'Failed to publish content with id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#emptyRecycleBin * @methodOf umbraco.resources.contentResource @@ -893,16 +710,10 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - emptyRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); - }, - - /** + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'EmptyRecycleBin')), 'Failed to empty the recycle bin'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#deleteById * @methodOf umbraco.resources.contentResource @@ -922,17 +733,13 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - deleteById: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - }, - - /** + deleteById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to delete item ' + id); + }, + deleteBlueprint: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'DeleteBlueprint', [{ id: id }])), 'Failed to delete blueprint ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#getById * @methodOf umbraco.resources.contentResource @@ -953,17 +760,13 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the content item. * */ - getById: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for content id ' + id); - }, - - /** + getById: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetById', [{ id: id }])), 'Failed to retrieve data for content id ' + id); + }, + getBlueprintById: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetBlueprintById', [{ id: id }])), 'Failed to retrieve data for content id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#getByIds * @methodOf umbraco.resources.contentResource @@ -984,24 +787,14 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the content items array. * */ - getByIds: function (ids) { - - var idQuery = ""; - _.each(ids, function (item) { - idQuery += "ids=" + item + "&"; - }); - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for content with multiple ids'); - }, - - - /** + getByIds: function (ids) { + var idQuery = ''; + _.each(ids, function (item) { + idQuery += 'ids=' + item + '&'; + }); + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetByIds', idQuery)), 'Failed to retrieve data for content with multiple ids'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#getScaffold * @methodOf umbraco.resources.contentResource @@ -1033,18 +826,19 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the content scaffold. * */ - getScaffold: function (parentId, alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty content item type ' + alias); - }, - - /** + getScaffold: function (parentId, alias) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetEmpty', [ + { contentTypeAlias: alias }, + { parentId: parentId } + ])), 'Failed to retrieve data for empty content item type ' + alias); + }, + getBlueprintScaffold: function (parentId, blueprintId) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetEmpty', [ + { blueprintId: blueprintId }, + { parentId: parentId } + ])), 'Failed to retrieve blueprint for id ' + blueprintId); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#getNiceUrl * @methodOf umbraco.resources.contentResource @@ -1064,16 +858,10 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the url. * */ - getNiceUrl: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetNiceUrl", [{ id: id }])), - 'Failed to retrieve url for id:' + id); - }, - - /** + getNiceUrl: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetNiceUrl', [{ id: id }])), 'Failed to retrieve url for id:' + id); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#getChildren * @methodOf umbraco.resources.contentResource @@ -1100,63 +888,52 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing an array of content items. * */ - getChildren: function (parentId, options) { - - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder", - orderBySystemField: true - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - //converts the value to a js bool - function toBool(v) { - if (angular.isNumber(v)) { - return v > 0; + getChildren: function (parentId, options) { + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: 'Ascending', + orderBy: 'SortOrder', + orderBySystemField: true + }; + if (options === undefined) { + options = {}; } - if (angular.isString(v)) { - return v === "true"; + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === 'asc') { + options.orderDirection = 'Ascending'; + } else if (options.orderDirection === 'desc') { + options.orderDirection = 'Descending'; } - if (typeof v === "boolean") { - return v; + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === 'true'; + } + if (typeof v === 'boolean') { + return v; + } + return false; } - return false; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetChildren", - { - id: parentId, - pageNumber: options.pageNumber, - pageSize: options.pageSize, - orderBy: options.orderBy, - orderDirection: options.orderDirection, - orderBySystemField: toBool(options.orderBySystemField), - filter: options.filter - })), - 'Failed to retrieve children for content item ' + parentId); - }, - - /** + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetChildren', { + id: parentId, + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection, + orderBySystemField: toBool(options.orderBySystemField), + filter: options.filter + })), 'Failed to retrieve children for content item ' + parentId); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#hasPermission * @methodOf umbraco.resources.contentResource @@ -1178,27 +955,19 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - checkPermission: function (permission, id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "HasPermission", - [{ permissionToCheck: permission }, { nodeId: id }])), - 'Failed to check permission for item ' + id); - }, - - getPermissions: function (nodeIds) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetPermissions"), - nodeIds), - 'Failed to get permissions'); - }, - - /** + checkPermission: function (permission, id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'HasPermission', [ + { permissionToCheck: permission }, + { nodeId: id } + ])), 'Failed to check permission for item ' + id); + }, + getDetailedPermissions: function (contentId) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetDetailedPermissions', { contentId: contentId })), 'Failed to retrieve permissions for content item ' + contentId); + }, + getPermissions: function (nodeIds) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetPermissions'), nodeIds), 'Failed to get permissions'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#save * @methodOf umbraco.resources.contentResource @@ -1226,12 +995,15 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the saved content item. * */ - save: function (content, isNew, files) { - return saveContentItem(content, "save" + (isNew ? "New" : ""), files); - }, - - - /** + save: function (content, isNew, files) { + var endpoint = umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSave'); + return saveContentItem(content, 'save' + (isNew ? 'New' : ''), files, endpoint); + }, + saveBlueprint: function (content, isNew, files) { + var endpoint = umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSaveBlueprint'); + return saveContentItem(content, 'save' + (isNew ? 'New' : ''), files, endpoint); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#publish * @methodOf umbraco.resources.contentResource @@ -1259,12 +1031,11 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the saved content item. * */ - publish: function (content, isNew, files) { - return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); - }, - - - /** + publish: function (content, isNew, files) { + var endpoint = umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSave'); + return saveContentItem(content, 'publish' + (isNew ? 'New' : ''), files, endpoint); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#sendToPublish * @methodOf umbraco.resources.contentResource @@ -1290,11 +1061,11 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the saved content item. * */ - sendToPublish: function (content, isNew, files) { - return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); - }, - - /** + sendToPublish: function (content, isNew, files) { + var endpoint = umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSave'); + return saveContentItem(content, 'sendPublish' + (isNew ? 'New' : ''), files, endpoint); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#publishByid * @methodOf umbraco.resources.contentResource @@ -1314,71 +1085,46 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the published content item. * */ - publishById: function (id) { - - if (!id) { - throw "id cannot be null"; + publishById: function (id) { + if (!id) { + throw 'id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostPublishById', [{ id: id }])), 'Failed to publish content with id ' + id); + }, + createBlueprintFromContent: function (contentId, name) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'CreateBlueprintFromContent', { + contentId: contentId, + name: name + })), 'Failed to create blueprint from content with id ' + contentId); } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostPublishById", - [{ id: id }])), - 'Failed to publish content with id ' + id); - - } - - - }; -} - -angular.module('umbraco.resources').factory('contentResource', contentResource); - -/** + }; + } + angular.module('umbraco.resources').factory('contentResource', contentResource); + /** * @ngdoc service * @name umbraco.resources.contentTypeResource * @description Loads in data for content types **/ -function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { - - return { - - getCount: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetCount")), - 'Failed to retrieve count'); - }, - - getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { - if (!filterContentTypes) { - filterContentTypes = []; - } - if (!filterPropertyTypes) { - filterPropertyTypes = []; - } - - var query = { - contentTypeId: contentTypeId, - filterContentTypes: filterContentTypes, - filterPropertyTypes: filterPropertyTypes - }; - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetAvailableCompositeContentTypes"), - query), - 'Failed to retrieve data for content type id ' + contentTypeId); - }, - - - /** + function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { + return { + getCount: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetCount')), 'Failed to retrieve count'); + }, + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + var query = { + contentTypeId: contentTypeId, + filterContentTypes: filterContentTypes, + filterPropertyTypes: filterPropertyTypes + }; + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetAvailableCompositeContentTypes'), query), 'Failed to retrieve data for content type id ' + contentTypeId); + }, + /** * @ngdoc method * @name umbraco.resources.contentTypeResource#getAllowedTypes * @methodOf umbraco.resources.contentTypeResource @@ -1397,19 +1143,10 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * @returns {Promise} resourcePromise object. * */ - getAllowedTypes: function (contentTypeId) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetAllowedChildren", - [{ contentId: contentTypeId }])), - 'Failed to retrieve data for content id ' + contentTypeId); - }, - - - /** + getAllowedTypes: function (contentTypeId) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetAllowedChildren', [{ contentId: contentTypeId }])), 'Failed to retrieve data for content id ' + contentTypeId); + }, + /** * @ngdoc method * @name umbraco.resources.contentTypeResource#getAllPropertyTypeAliases * @methodOf umbraco.resources.contentTypeResource @@ -1420,70 +1157,25 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * @returns {Promise} resourcePromise object. * */ - getAllPropertyTypeAliases: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetAllPropertyTypeAliases")), - 'Failed to retrieve property type aliases'); - }, - - getAllStandardFields: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetAllStandardFields")), - 'Failed to retrieve standard fields'); - }, - - getPropertyTypeScaffold : function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetPropertyTypeScaffold", - [{ id: id }])), - 'Failed to retrieve property type scaffold'); - }, - - getById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve content type'); - }, - - deleteById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete content type'); - }, - - deleteContainerById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "DeleteContainer", - [{ id: id }])), - 'Failed to delete content type contaier'); - }, - - /** + getAllPropertyTypeAliases: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetAllPropertyTypeAliases')), 'Failed to retrieve property type aliases'); + }, + getAllStandardFields: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetAllStandardFields')), 'Failed to retrieve standard fields'); + }, + getPropertyTypeScaffold: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetPropertyTypeScaffold', [{ id: id }])), 'Failed to retrieve property type scaffold'); + }, + getById: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetById', [{ id: id }])), 'Failed to retrieve content type'); + }, + deleteById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to delete content type'); + }, + deleteContainerById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'DeleteContainer', [{ id: id }])), 'Failed to delete content type contaier'); + }, + /** * @ngdoc method * @name umbraco.resources.contentTypeResource#getAll * @methodOf umbraco.resources.contentTypeResource @@ -1494,27 +1186,13 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * @returns {Promise} resourcePromise object. * */ - getAll: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetAll")), - 'Failed to retrieve all content types'); - }, - - getScaffold: function (parentId) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetEmpty", { parentId: parentId })), - 'Failed to retrieve content type scaffold'); - }, - - /** + getAll: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetAll')), 'Failed to retrieve all content types'); + }, + getScaffold: function (parentId) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetEmpty', { parentId: parentId })), 'Failed to retrieve content type scaffold'); + }, + /** * @ngdoc method * @name umbraco.resources.contentTypeResource#save * @methodOf umbraco.resources.contentTypeResource @@ -1526,16 +1204,11 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * @returns {Promise} resourcePromise object. * */ - save: function (contentType) { - - var saveModel = umbDataFormatter.formatContentTypePostData(contentType); - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostSave"), saveModel), - 'Failed to save data for content type id ' + contentType.id); - }, - - /** + save: function (contentType) { + var saveModel = umbDataFormatter.formatContentTypePostData(contentType); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostSave'), saveModel), 'Failed to save data for content type id ' + contentType.id); + }, + /** * @ngdoc method * @name umbraco.resources.contentTypeResource#move * @methodOf umbraco.resources.contentTypeResource @@ -1558,71 +1231,68 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * @returns {Promise} resourcePromise object. * */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move content'); - }, - - copy: function(args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; + move: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.id) { + throw 'args.id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostMove'), { + parentId: args.parentId, + id: args.id + }), 'Failed to move content'); + }, + copy: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.id) { + throw 'args.id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostCopy'), { + parentId: args.parentId, + id: args.id + }), 'Failed to copy content'); + }, + createContainer: function (parentId, name) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostCreateContainer', { + parentId: parentId, + name: name + })), 'Failed to create a folder under parent id ' + parentId); + }, + renameContainer: function (id, name) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostRenameContainer', { + id: id, + name: name + })), 'Failed to rename the folder with id ' + id); } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCopy"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to copy content'); - }, - - createContainer: function(parentId, name) { - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateContainer", { parentId: parentId, name: name })), - 'Failed to create a folder under parent id ' + parentId); - - } - - }; -} -angular.module('umbraco.resources').factory('contentTypeResource', contentTypeResource); - -/** + }; + } + angular.module('umbraco.resources').factory('contentTypeResource', contentTypeResource); + /** * @ngdoc service * @name umbraco.resources.currentUserResource * @description Used for read/updates for the currently logged in user * * **/ -function currentUserResource($q, $http, umbRequestHelper) { - - //the factory object returned - return { - - /** + function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) { + //the factory object returned + return { + performSetInvitedUserPassword: function (newPassword) { + if (!newPassword) { + return angularHelper.rejectedPromise({ errorMsg: 'newPassword cannot be empty' }); + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('currentUserApiBaseUrl', 'PostSetInvitedUserPassword'), angular.toJson(newPassword)), 'Failed to change password'); + }, + /** * @ngdoc method * @name umbraco.resources.currentUserResource#changePassword * @methodOf umbraco.resources.currentUserResource @@ -1633,47 +1303,25 @@ function currentUserResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the user array. * */ - changePassword: function (changePasswordArgs) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "currentUserApiBaseUrl", - "PostChangePassword"), - changePasswordArgs), - 'Failed to change password'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.currentUserResource#getMembershipProviderConfig - * @methodOf umbraco.resources.currentUserResource - * - * @description - * Gets the configuration of the user membership provider which is used to configure the change password form - */ - getMembershipProviderConfig: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "currentUserApiBaseUrl", - "GetMembershipProviderConfig")), - 'Failed to retrieve membership provider config'); - }, - }; -} - -angular.module('umbraco.resources').factory('currentUserResource', currentUserResource); - -/** + changePassword: function (changePasswordArgs) { + changePasswordArgs = umbDataFormatter.formatChangePasswordModel(changePasswordArgs); + if (!changePasswordArgs) { + throw 'No password data to change'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('currentUserApiBaseUrl', 'PostChangePassword'), changePasswordArgs), 'Failed to change password'); + } + }; + } + angular.module('umbraco.resources').factory('currentUserResource', currentUserResource); + /** * @ngdoc service * @name umbraco.resources.dashboardResource * @description Handles loading the dashboard manifest **/ -function dashboardResource($q, $http, umbRequestHelper) { - //the factory object returned - return { - - /** + function dashboardResource($q, $http, umbRequestHelper) { + //the factory object returned + return { + /** * @ngdoc method * @name umbraco.resources.dashboardResource#getDashboard * @methodOf umbraco.resources.dashboardResource @@ -1685,17 +1333,10 @@ function dashboardResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the user array. * */ - getDashboard: function (section) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dashboardApiBaseUrl", - "GetDashboard", - [{ section: section }])), - 'Failed to get dashboard ' + section); - }, - - /** + getDashboard: function (section) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dashboardApiBaseUrl', 'GetDashboard', [{ section: section }])), 'Failed to get dashboard ' + section); + }, + /** * @ngdoc method * @name umbraco.resources.dashboardResource#getRemoteDashboardContent * @methodOf umbraco.resources.dashboardResource @@ -1707,53 +1348,33 @@ function dashboardResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the user array. * */ - getRemoteDashboardContent: function (section, baseurl) { - - //build request values with optional params - var values = [{ section: section }]; - if (baseurl) - { - values.push({ baseurl: baseurl }); - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dashboardApiBaseUrl", - "GetRemoteDashboardContent", - values)), "Failed to get dashboard content"); - }, - - getRemoteDashboardCssUrl: function (section, baseurl) { - - //build request values with optional params - var values = [{ section: section }]; - if (baseurl) { - values.push({ baseurl: baseurl }); + getRemoteDashboardContent: function (section, baseurl) { + //build request values with optional params + var values = [{ section: section }]; + if (baseurl) { + values.push({ baseurl: baseurl }); + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dashboardApiBaseUrl', 'GetRemoteDashboardContent', values)), 'Failed to get dashboard content'); + }, + getRemoteDashboardCssUrl: function (section, baseurl) { + //build request values with optional params + var values = [{ section: section }]; + if (baseurl) { + values.push({ baseurl: baseurl }); + } + return umbRequestHelper.getApiUrl('dashboardApiBaseUrl', 'GetRemoteDashboardCss', values); } - - return umbRequestHelper.getApiUrl( - "dashboardApiBaseUrl", - "GetRemoteDashboardCss", - values); - } - - - - }; -} - -angular.module('umbraco.resources').factory('dashboardResource', dashboardResource); -/** + }; + } + angular.module('umbraco.resources').factory('dashboardResource', dashboardResource); + /** * @ngdoc service * @name umbraco.resources.dataTypeResource * @description Loads in data for data types **/ -function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { - - return { - - /** + function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { + return { + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#getPreValues * @methodOf umbraco.resources.dataTypeResource @@ -1774,22 +1395,16 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getPreValues: function (editorAlias, dataTypeId) { - - if (!dataTypeId) { - dataTypeId = -1; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "GetPreValues", - [{ editorAlias: editorAlias }, { dataTypeId: dataTypeId }])), - "Failed to retrieve pre values for editor alias " + editorAlias); - }, - - /** + getPreValues: function (editorAlias, dataTypeId) { + if (!dataTypeId) { + dataTypeId = -1; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'GetPreValues', [ + { editorAlias: editorAlias }, + { dataTypeId: dataTypeId } + ])), 'Failed to retrieve pre values for editor alias ' + editorAlias); + }, + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#getById * @methodOf umbraco.resources.dataTypeResource @@ -1809,18 +1424,10 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "GetById", - [{ id: id }])), - "Failed to retrieve data for data type id " + id); - }, - - /** + getById: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'GetById', [{ id: id }])), 'Failed to retrieve data for data type id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#getByName * @methodOf umbraco.resources.dataTypeResource @@ -1840,56 +1447,22 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getByName: function (name) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "GetByName", - [{ name: name }])), - "Failed to retrieve data for data type with name: " + name); - }, - - getAll: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "GetAll")), - "Failed to retrieve data"); - }, - - getGroupedDataTypes: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "GetGroupedDataTypes")), - "Failed to retrieve data"); - }, - - getGroupedPropertyEditors : function(){ - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "GetGroupedPropertyEditors")), - "Failed to retrieve data"); - }, - - getAllPropertyEditors: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "GetAllPropertyEditors")), - "Failed to retrieve data"); - }, - - /** + getByName: function (name) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'GetByName', [{ name: name }])), 'Failed to retrieve data for data type with name: ' + name); + }, + getAll: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'GetAll')), 'Failed to retrieve data'); + }, + getGroupedDataTypes: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'GetGroupedDataTypes')), 'Failed to retrieve data'); + }, + getGroupedPropertyEditors: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'GetGroupedPropertyEditors')), 'Failed to retrieve data'); + }, + getAllPropertyEditors: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'GetAllPropertyEditors')), 'Failed to retrieve data'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#getScaffold * @methodOf umbraco.resources.contentResource @@ -1916,16 +1489,10 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the data type scaffold. * */ - getScaffold: function (parentId) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "GetEmpty", { parentId: parentId })), - "Failed to retrieve data for empty datatype"); - }, - /** + getScaffold: function (parentId) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'GetEmpty', { parentId: parentId })), 'Failed to retrieve data for empty datatype'); + }, + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#deleteById * @methodOf umbraco.resources.dataTypeResource @@ -1945,30 +1512,13 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - deleteById: function(id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "DeleteById", - [{ id: id }])), - "Failed to delete item " + id); - }, - - deleteContainerById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "DeleteContainer", - [{ id: id }])), - 'Failed to delete content type contaier'); - }, - - - - /** + deleteById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to delete item ' + id); + }, + deleteContainerById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'DeleteContainer', [{ id: id }])), 'Failed to delete content type contaier'); + }, + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#getCustomListView * @methodOf umbraco.resources.dataTypeResource @@ -1987,19 +1537,10 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the listview datatype. * */ - - getCustomListView: function (contentTypeAlias) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "GetCustomListView", - { contentTypeAlias: contentTypeAlias } - )), - "Failed to retrieve data for custom listview datatype"); - }, - - /** + getCustomListView: function (contentTypeAlias) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'GetCustomListView', { contentTypeAlias: contentTypeAlias })), 'Failed to retrieve data for custom listview datatype'); + }, + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#createCustomListView * @methodOf umbraco.resources.dataTypeResource @@ -2017,18 +1558,10 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the listview datatype. * */ - createCustomListView: function (contentTypeAlias) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "PostCreateCustomListView", - { contentTypeAlias: contentTypeAlias } - )), - "Failed to create a custom listview datatype"); - }, - - /** + createCustomListView: function (contentTypeAlias) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'PostCreateCustomListView', { contentTypeAlias: contentTypeAlias })), 'Failed to create a custom listview datatype'); + }, + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#save * @methodOf umbraco.resources.dataTypeResource @@ -2042,16 +1575,11 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - save: function (dataType, preValues, isNew) { - - var saveModel = umbDataFormatter.formatDataTypePostData(dataType, preValues, "save" + (isNew ? "New" : "")); - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("dataTypeApiBaseUrl", "PostSave"), saveModel), - "Failed to save data for data type id " + dataType.id); - }, - - /** + save: function (dataType, preValues, isNew) { + var saveModel = umbDataFormatter.formatDataTypePostData(dataType, preValues, 'save' + (isNew ? 'New' : '')); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'PostSave'), saveModel), 'Failed to save data for data type id ' + dataType.id); + }, + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#move * @methodOf umbraco.resources.dataTypeResource @@ -2074,42 +1602,37 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; + move: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.id) { + throw 'args.id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'PostMove'), { + parentId: args.parentId, + id: args.id + }), 'Failed to move content'); + }, + createContainer: function (parentId, name) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'PostCreateContainer', { + parentId: parentId, + name: name + })), 'Failed to create a folder under parent id ' + parentId); + }, + renameContainer: function (id, name) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('dataTypeApiBaseUrl', 'PostRenameContainer', { + id: id, + name: name + })), 'Failed to rename the folder with id ' + id); } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("dataTypeApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move content'); - }, - - createContainer: function (parentId, name) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "dataTypeApiBaseUrl", - "PostCreateContainer", - { parentId: parentId, name: name })), - 'Failed to create a folder under parent id ' + parentId); - } - }; -} - -angular.module("umbraco.resources").factory("dataTypeResource", dataTypeResource); - -/** + }; + } + angular.module('umbraco.resources').factory('dataTypeResource', dataTypeResource); + /** * @ngdoc service * @name umbraco.resources.entityResource * @description Loads in basic data for all entities @@ -2142,26 +1665,20 @@ angular.module("umbraco.resources").factory("dataTypeResource", dataTypeResource * - Domain * - DataType **/ -function entityResource($q, $http, umbRequestHelper) { - - //the factory object returned - return { - - getSafeAlias: function (value, camelCase) { - - if (!value) { - return ""; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetSafeAlias", { value: value, camelCase: camelCase })), - 'Failed to retrieve content type scaffold'); - }, - - /** + function entityResource($q, $http, umbRequestHelper) { + //the factory object returned + return { + getSafeAlias: function (value, camelCase) { + if (!value) { + return ''; + } + value = value.replace('#', ''); + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetSafeAlias', { + value: value, + camelCase: camelCase + })), 'Failed to retrieve content type scaffold'); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getPath * @methodOf umbraco.resources.entityResource @@ -2182,22 +1699,16 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the url. * */ - getPath: function (id, type) { - - if (id === -1 || id === "-1") { - return "-1"; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetPath", - [{ id: id }, {type: type }])), - 'Failed to retrieve path for id:' + id); - }, - - /** + getPath: function (id, type) { + if (id === -1 || id === '-1') { + return '-1'; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetPath', [ + { id: id }, + { type: type } + ])), 'Failed to retrieve path for id:' + id); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getUrl * @methodOf umbraco.resources.entityResource @@ -2218,22 +1729,16 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the url. * */ - getUrl: function (id, type) { - - if (id === -1 || id === "-1") { - return ""; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetUrl", - [{ id: id }, {type: type }])), - 'Failed to retrieve url for id:' + id); - }, - - /** + getUrl: function (id, type) { + if (id === -1 || id === '-1') { + return ''; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetUrl', [ + { id: id }, + { type: type } + ])), 'Failed to retrieve url for id:' + id); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getById * @methodOf umbraco.resources.entityResource @@ -2256,22 +1761,16 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity. * */ - getById: function (id, type) { - - if (id === -1 || id === "-1") { - return null; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetById", - [{ id: id }, { type: type }])), - 'Failed to retrieve entity data for id ' + id); - }, - - /** + getById: function (id, type) { + if (id === -1 || id === '-1') { + return null; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetById', [ + { id: id }, + { type: type } + ])), 'Failed to retrieve entity data for id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getByIds * @methodOf umbraco.resources.entityResource @@ -2294,23 +1793,11 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity array. * */ - getByIds: function (ids, type) { - - var query = "type=" + type; - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetByIds", - query), - { - ids: ids - }), - 'Failed to retrieve entity data for ids ' + ids); - }, - - /** + getByIds: function (ids, type) { + var query = 'type=' + type; + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetByIds', query), { ids: ids }), 'Failed to retrieve entity data for ids ' + ids); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getByQuery * @methodOf umbraco.resources.entityResource @@ -2334,17 +1821,14 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity. * */ - getByQuery: function (query, nodeContextId, type) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetByQuery", - [{ query: query }, { nodeContextId: nodeContextId }, { type: type }])), - 'Failed to retrieve entity data for query ' + query); - }, - - /** + getByQuery: function (query, nodeContextId, type) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetByQuery', [ + { query: query }, + { nodeContextId: nodeContextId }, + { type: type } + ])), 'Failed to retrieve entity data for query ' + query); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getAll * @methodOf umbraco.resources.entityResource @@ -2369,28 +1853,19 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity. * */ - getAll: function (type, postFilter, postFilterParams) { - - //need to build the query string manually - var query = "type=" + type + "&postFilter=" + (postFilter ? postFilter : ""); - if (postFilter && postFilterParams) { - var counter = 0; - _.each(postFilterParams, function(val, key) { - query += "&postFilterParams[" + counter + "].key=" + key + "&postFilterParams[" + counter + "].value=" + val; - counter++; - }); - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetAll", - query)), - 'Failed to retrieve entity data for type ' + type); - }, - - /** + getAll: function (type, postFilter, postFilterParams) { + //need to build the query string manually + var query = 'type=' + type + '&postFilter=' + (postFilter ? postFilter : ''); + if (postFilter && postFilterParams) { + var counter = 0; + _.each(postFilterParams, function (val, key) { + query += '&postFilterParams[' + counter + '].key=' + key + '&postFilterParams[' + counter + '].value=' + val; + counter++; + }); + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetAll', query)), 'Failed to retrieve entity data for type ' + type); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getAncestors * @methodOf umbraco.resources.entityResource @@ -2403,17 +1878,13 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity. * */ - getAncestors: function (id, type) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetAncestors", - [{id: id}, {type: type}])), - 'Failed to retrieve ancestor data for id ' + id); - }, - - /** + getAncestors: function (id, type) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetAncestors', [ + { id: id }, + { type: type } + ])), 'Failed to retrieve ancestor data for id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getChildren * @methodOf umbraco.resources.entityResource @@ -2426,18 +1897,13 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity. * */ - getChildren: function (id, type) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetChildren", - [{ id: id }, { type: type }])), - 'Failed to retrieve child data for id ' + id); - }, - - /** + getChildren: function (id, type) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetChildren', [ + { id: id }, + { type: type } + ])), 'Failed to retrieve child data for id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getPagedChildren * @methodOf umbraco.resources.entityResource @@ -2465,49 +1931,38 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing an array of content items. * */ - getPagedChildren: function (parentId, type, options) { - - var defaults = { - pageSize: 1, - pageNumber: 100, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetPagedChildren", - { - id: parentId, - type: type, - pageNumber: options.pageNumber, - pageSize: options.pageSize, - orderBy: options.orderBy, - orderDirection: options.orderDirection, - filter: encodeURIComponent(options.filter) - } - )), - 'Failed to retrieve child data for id ' + parentId); - }, - - /** + getPagedChildren: function (parentId, type, options) { + var defaults = { + pageSize: 1, + pageNumber: 100, + filter: '', + orderDirection: 'Ascending', + orderBy: 'SortOrder' + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === 'asc') { + options.orderDirection = 'Ascending'; + } else if (options.orderDirection === 'desc') { + options.orderDirection = 'Descending'; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetPagedChildren', { + id: parentId, + type: type, + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection, + filter: encodeURIComponent(options.filter) + })), 'Failed to retrieve child data for id ' + parentId); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getPagedDescendants * @methodOf umbraco.resources.entityResource @@ -2535,49 +1990,38 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing an array of content items. * */ - getPagedDescendants: function (parentId, type, options) { - - var defaults = { - pageSize: 1, - pageNumber: 100, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetPagedDescendants", - { - id: parentId, - type: type, - pageNumber: options.pageNumber, - pageSize: options.pageSize, - orderBy: options.orderBy, - orderDirection: options.orderDirection, - filter: encodeURIComponent(options.filter) - } - )), - 'Failed to retrieve child data for id ' + parentId); - }, - - /** + getPagedDescendants: function (parentId, type, options) { + var defaults = { + pageSize: 1, + pageNumber: 100, + filter: '', + orderDirection: 'Ascending', + orderBy: 'SortOrder' + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === 'asc') { + options.orderDirection = 'Ascending'; + } else if (options.orderDirection === 'desc') { + options.orderDirection = 'Descending'; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetPagedDescendants', { + id: parentId, + type: type, + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection, + filter: encodeURIComponent(options.filter) + })), 'Failed to retrieve child data for id ' + parentId); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#search * @methodOf umbraco.resources.entityResource @@ -2599,30 +2043,21 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity array. * */ - search: function (query, type, searchFrom, canceler) { - - var args = [{ query: query }, { type: type }]; - if (searchFrom) { - args.push({ searchFrom: searchFrom }); - } - - var httpConfig = {}; - if (canceler) { - httpConfig["timeout"] = canceler; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "Search", - args), - httpConfig), - 'Failed to retrieve entity data for query ' + query); - }, - - - /** + search: function (query, type, searchFrom, canceler) { + var args = [ + { query: query }, + { type: type } + ]; + if (searchFrom) { + args.push({ searchFrom: searchFrom }); + } + var httpConfig = {}; + if (canceler) { + httpConfig['timeout'] = canceler; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'Search', args), httpConfig), 'Failed to retrieve entity data for query ' + query); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#searchAll * @methodOf umbraco.resources.entityResource @@ -2643,29 +2078,17 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity array. * */ - searchAll: function (query, canceler) { - - var httpConfig = {}; - if (canceler) { - httpConfig["timeout"] = canceler; + searchAll: function (query, canceler) { + var httpConfig = {}; + if (canceler) { + httpConfig['timeout'] = canceler; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'SearchAll', [{ query: query }]), httpConfig), 'Failed to retrieve entity data for query ' + query); } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "SearchAll", - [{ query: query }]), - httpConfig), - 'Failed to retrieve entity data for query ' + query); - } - - }; -} - -angular.module('umbraco.resources').factory('entityResource', entityResource); - -/** + }; + } + angular.module('umbraco.resources').factory('entityResource', entityResource); + /** * @ngdoc service * @name umbraco.resources.healthCheckResource * @function @@ -2673,12 +2096,10 @@ angular.module('umbraco.resources').factory('entityResource', entityResource); * @description * Used by the health check dashboard to get checks and send requests to fix checks. */ -(function () { - 'use strict'; - - function healthCheckResource($http, umbRequestHelper) { - - /** + (function () { + 'use strict'; + function healthCheckResource($http, umbRequestHelper) { + /** * @ngdoc function * @name umbraco.resources.healthCheckService#getAllChecks * @methodOf umbraco.resources.healthCheckResource @@ -2687,14 +2108,10 @@ angular.module('umbraco.resources').factory('entityResource', entityResource); * @description * Called to get all available health checks */ - function getAllChecks() { - return umbRequestHelper.resourcePromise( - $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + "GetAllHealthChecks"), - "Failed to retrieve health checks" - ); - } - - /** + function getAllChecks() { + return umbRequestHelper.resourcePromise($http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'GetAllHealthChecks'), 'Failed to retrieve health checks'); + } + /** * @ngdoc function * @name umbraco.resources.healthCheckService#getStatus * @methodOf umbraco.resources.healthCheckResource @@ -2703,14 +2120,10 @@ angular.module('umbraco.resources').factory('entityResource', entityResource); * @description * Called to get execute a health check and return the check status */ - function getStatus(id) { - return umbRequestHelper.resourcePromise( - $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'GetStatus?id=' + id), - 'Failed to retrieve status for health check with ID ' + id - ); - } - - /** + function getStatus(id) { + return umbRequestHelper.resourcePromise($http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'GetStatus?id=' + id), 'Failed to retrieve status for health check with ID ' + id); + } + /** * @ngdoc function * @name umbraco.resources.healthCheckService#executeAction * @methodOf umbraco.resources.healthCheckResource @@ -2719,71 +2132,51 @@ angular.module('umbraco.resources').factory('entityResource', entityResource); * @description * Called to execute a health check action (rectifying an issue) */ - function executeAction(action) { - return umbRequestHelper.resourcePromise( - $http.post(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'ExecuteAction', action), - 'Failed to execute action with alias ' + action.alias + ' and healthCheckId + ' + action.healthCheckId - ); - } - - var resource = { - getAllChecks: getAllChecks, - getStatus: getStatus, - executeAction: executeAction - }; - - return resource; - - } - - - angular.module('umbraco.resources').factory('healthCheckResource', healthCheckResource); - - -})(); - -/** + function executeAction(action) { + return umbRequestHelper.resourcePromise($http.post(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'ExecuteAction', action), 'Failed to execute action with alias ' + action.alias + ' and healthCheckId + ' + action.healthCheckId); + } + var resource = { + getAllChecks: getAllChecks, + getStatus: getStatus, + executeAction: executeAction + }; + return resource; + } + angular.module('umbraco.resources').factory('healthCheckResource', healthCheckResource); + }()); + /** * @ngdoc service * @name umbraco.resources.legacyResource * @description Handles legacy dialog requests **/ -function legacyResource($q, $http, umbRequestHelper) { - - //the factory object returned - return { - /** Loads in the data to display the section list */ - deleteItem: function (args) { - - if (!args.nodeId || !args.nodeType || !args.alias) { - throw "The args parameter is not formatted correct, it requires properties: nodeId, nodeType, alias"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "legacyApiBaseUrl", - "DeleteLegacyItem", - [{ nodeId: args.nodeId }, { nodeType: args.nodeType }, { alias: args.alias }])), - 'Failed to delete item ' + args.nodeId); - - } - }; -} - -angular.module('umbraco.resources').factory('legacyResource', legacyResource); -/** + function legacyResource($q, $http, umbRequestHelper) { + //the factory object returned + return { + /** Loads in the data to display the section list */ + deleteItem: function (args) { + if (!args.nodeId || !args.nodeType || !args.alias) { + throw 'The args parameter is not formatted correct, it requires properties: nodeId, nodeType, alias'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('legacyApiBaseUrl', 'DeleteLegacyItem', [ + { nodeId: args.nodeId }, + { nodeType: args.nodeType }, + { alias: args.alias } + ])), 'Failed to delete item ' + args.nodeId); + } + }; + } + angular.module('umbraco.resources').factory('legacyResource', legacyResource); + /** * @ngdoc service * @name umbraco.resources.logResource * @description Retrives log history from umbraco * * **/ -function logResource($q, $http, umbRequestHelper) { - - //the factory object returned - return { - - /** + function logResource($q, $http, umbRequestHelper) { + //the factory object returned + return { + /** * @ngdoc method * @name umbraco.resources.logResource#getEntityLog * @methodOf umbraco.resources.logResource @@ -2803,17 +2196,10 @@ function logResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the log. * */ - getEntityLog: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "logApiBaseUrl", - "GetEntityLog", - [{ id: id }])), - 'Failed to retrieve user data for id ' + id); - }, - - /** + getEntityLog: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('logApiBaseUrl', 'GetEntityLog', [{ id: id }])), 'Failed to retrieve user data for id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.logResource#getUserLog * @methodOf umbraco.resources.logResource @@ -2834,17 +2220,13 @@ function logResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the log. * */ - getUserLog: function (type, since) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "logApiBaseUrl", - "GetCurrentUserLog", - [{ logtype: type, sinceDate: since }])), - 'Failed to retrieve user data for id ' + id); - }, - - /** + getUserLog: function (type, since) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('logApiBaseUrl', 'GetCurrentUserLog', [ + { logtype: type }, + { sinceDate: since } + ])), 'Failed to retrieve log data for current user of type ' + type + ' since ' + since); + }, + /** * @ngdoc method * @name umbraco.resources.logResource#getLog * @methodOf umbraco.resources.logResource @@ -2865,32 +2247,25 @@ function logResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the log. * */ - getLog: function (type, since) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "logApiBaseUrl", - "GetLog", - [{ logtype: type, sinceDate: since }])), - 'Failed to retrieve user data for id ' + id); - } - }; -} - -angular.module('umbraco.resources').factory('logResource', logResource); - -/** + getLog: function (type, since) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('logApiBaseUrl', 'GetLog', [ + { logtype: type }, + { sinceDate: since } + ])), 'Failed to retrieve log data of type ' + type + ' since ' + since); + } + }; + } + angular.module('umbraco.resources').factory('logResource', logResource); + /** * @ngdoc service * @name umbraco.resources.macroResource * @description Deals with data for macros * **/ -function macroResource($q, $http, umbRequestHelper) { - - //the factory object returned - return { - - /** + function macroResource($q, $http, umbRequestHelper) { + //the factory object returned + return { + /** * @ngdoc method * @name umbraco.resources.macroResource#getMacroParameters * @methodOf umbraco.resources.macroResource @@ -2901,17 +2276,10 @@ function macroResource($q, $http, umbRequestHelper) { * @param {int} macroId The macro id to get parameters for * */ - getMacroParameters: function (macroId) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "macroApiBaseUrl", - "GetMacroParameters", - [{ macroId: macroId }])), - 'Failed to retrieve macro parameters for macro with id ' + macroId); - }, - - /** + getMacroParameters: function (macroId) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('macroApiBaseUrl', 'GetMacroParameters', [{ macroId: macroId }])), 'Failed to retrieve macro parameters for macro with id ' + macroId); + }, + /** * @ngdoc method * @name umbraco.resources.macroResource#getMacroResult * @methodOf umbraco.resources.macroResource @@ -2924,79 +2292,50 @@ function macroResource($q, $http, umbRequestHelper) { * @param {Array} macroParamDictionary A dictionary of macro parameters * */ - getMacroResultAsHtmlForEditor: function (macroAlias, pageId, macroParamDictionary) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "macroApiBaseUrl", - "GetMacroResultAsHtmlForEditor"), { - macroAlias: macroAlias, - pageId: pageId, - macroParams: macroParamDictionary - }), - 'Failed to retrieve macro result for macro with alias ' + macroAlias); - }, - - /** + getMacroResultAsHtmlForEditor: function (macroAlias, pageId, macroParamDictionary) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('macroApiBaseUrl', 'GetMacroResultAsHtmlForEditor'), { + macroAlias: macroAlias, + pageId: pageId, + macroParams: macroParamDictionary + }), 'Failed to retrieve macro result for macro with alias ' + macroAlias); + }, + /** * * @param {} filename * @returns {} */ - createPartialViewMacroWithFile: function(virtualPath, filename) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "macroApiBaseUrl", - "CreatePartialViewMacroWithFile"), { - virtualPath: virtualPath, - filename: filename - } - ), - 'Failed to create macro "' + filename + '"' - ); - - } - }; -} - -angular.module('umbraco.resources').factory('macroResource', macroResource); - -/** + createPartialViewMacroWithFile: function (virtualPath, filename) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('macroApiBaseUrl', 'CreatePartialViewMacroWithFile'), { + virtualPath: virtualPath, + filename: filename + }), 'Failed to create macro "' + filename + '"'); + } + }; + } + angular.module('umbraco.resources').factory('macroResource', macroResource); + /** * @ngdoc service * @name umbraco.resources.mediaResource * @description Loads in data for media **/ -function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { - - /** internal method process the saving of data and post processing the result */ - function saveMediaItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatMediaPostData(c, a); - } - }); - } - - return { - - getRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for media recycle bin'); - }, - - /** + function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { + /** internal method process the saving of data and post processing the result */ + function saveMediaItem(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'PostSave'), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMediaPostData(c, a); + } + }); + } + return { + getRecycleBin: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'GetRecycleBin')), 'Failed to retrieve data for media recycle bin'); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#sort * @methodOf umbraco.resources.mediaResource @@ -3018,27 +2357,22 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort media'); - }, - - /** + sort: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.sortedIds) { + throw 'args.sortedIds cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'PostSort'), { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), 'Failed to sort media'); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#move * @methodOf umbraco.resources.mediaResource @@ -3061,28 +2395,22 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move media'); - }, - - - /** + move: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.id) { + throw 'args.id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'PostMove'), { + parentId: args.parentId, + id: args.id + }), 'Failed to move media'); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#getById * @methodOf umbraco.resources.mediaResource @@ -3103,18 +2431,10 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the media item. * */ - getById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for media id ' + id); - }, - - /** + getById: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'GetById', [{ id: id }])), 'Failed to retrieve data for media id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#deleteById * @methodOf umbraco.resources.mediaResource @@ -3134,17 +2454,10 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - deleteById: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - }, - - /** + deleteById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to delete item ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#getByIds * @methodOf umbraco.resources.mediaResource @@ -3165,23 +2478,14 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the media items array. * */ - getByIds: function (ids) { - - var idQuery = ""; - _.each(ids, function (item) { - idQuery += "ids=" + item + "&"; - }); - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for media ids ' + ids); - }, - - /** + getByIds: function (ids) { + var idQuery = ''; + _.each(ids, function (item) { + idQuery += 'ids=' + item + '&'; + }); + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'GetByIds', idQuery)), 'Failed to retrieve data for media ids ' + ids); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#getScaffold * @methodOf umbraco.resources.mediaResource @@ -3213,30 +2517,16 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the media scaffold. * */ - getScaffold: function (parentId, alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty media item type ' + alias); - - }, - - rootMedia: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRootMedia")), - 'Failed to retrieve data for root media'); - - }, - - /** + getScaffold: function (parentId, alias) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'GetEmpty', [ + { contentTypeAlias: alias }, + { parentId: parentId } + ])), 'Failed to retrieve data for empty media item type ' + alias); + }, + rootMedia: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'GetRootMedia')), 'Failed to retrieve data for root media'); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#getChildren * @methodOf umbraco.resources.mediaResource @@ -3263,63 +2553,52 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing an array of content items. * */ - getChildren: function (parentId, options) { - - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder", - orderBySystemField: true - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - //converts the value to a js bool - function toBool(v) { - if (angular.isNumber(v)) { - return v > 0; + getChildren: function (parentId, options) { + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: 'Ascending', + orderBy: 'SortOrder', + orderBySystemField: true + }; + if (options === undefined) { + options = {}; } - if (angular.isString(v)) { - return v === "true"; + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === 'asc') { + options.orderDirection = 'Ascending'; + } else if (options.orderDirection === 'desc') { + options.orderDirection = 'Descending'; } - if (typeof v === "boolean") { - return v; + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === 'true'; + } + if (typeof v === 'boolean') { + return v; + } + return false; } - return false; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: toBool(options.orderBySystemField) }, - { filter: options.filter } - ])), - 'Failed to retrieve children for media item ' + parentId); - }, - - /** + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'GetChildren', [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: toBool(options.orderBySystemField) }, + { filter: options.filter } + ])), 'Failed to retrieve children for media item ' + parentId); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#save * @methodOf umbraco.resources.mediaResource @@ -3347,11 +2626,10 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the saved media item. * */ - save: function (media, isNew, files) { - return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); - }, - - /** + save: function (media, isNew, files) { + return saveMediaItem(media, 'save' + (isNew ? 'New' : ''), files); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#addFolder * @methodOf umbraco.resources.mediaResource @@ -3372,18 +2650,13 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - addFolder: function (name, parentId) { - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper - .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), - { - name: name, - parentId: parentId - }), - 'Failed to add folder'); - }, - - /** + addFolder: function (name, parentId) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'PostAddFolder'), { + name: name, + parentId: parentId + }), 'Failed to add folder'); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#getChildFolders * @methodOf umbraco.resources.mediaResource @@ -3407,24 +2680,14 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getChildFolders: function (parentId) { - if (!parentId) { - parentId = -1; - } - - //NOTE: This will return a max of 500 folders, if more is required it needs to be paged - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildFolders", - { - id: parentId - })), - 'Failed to retrieve child folders for media item ' + parentId); - }, - - /** + getChildFolders: function (parentId) { + if (!parentId) { + parentId = -1; + } + //NOTE: This will return a max of 500 folders, if more is required it needs to be paged + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'GetChildFolders', { id: parentId })), 'Failed to retrieve child folders for media item ' + parentId); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#emptyRecycleBin * @methodOf umbraco.resources.mediaResource @@ -3443,16 +2706,10 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - emptyRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); - }, - - /** + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'EmptyRecycleBin')), 'Failed to empty the recycle bin'); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#search * @methodOf umbraco.resources.mediaResource @@ -3475,71 +2732,43 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - search: function (query, pageNumber, pageSize, searchFrom) { - - var args = [ - { "query": query }, - { "pageNumber": pageNumber }, - { "pageSize": pageSize }, - { "searchFrom": searchFrom } - ]; - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "Search", - args)), - 'Failed to retrieve media items for search: ' + query); - } - - }; -} - -angular.module('umbraco.resources').factory('mediaResource', mediaResource); - -/** + search: function (query, pageNumber, pageSize, searchFrom) { + var args = [ + { 'query': query }, + { 'pageNumber': pageNumber }, + { 'pageSize': pageSize }, + { 'searchFrom': searchFrom } + ]; + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'Search', args)), 'Failed to retrieve media items for search: ' + query); + } + }; + } + angular.module('umbraco.resources').factory('mediaResource', mediaResource); + /** * @ngdoc service * @name umbraco.resources.mediaTypeResource * @description Loads in data for media types **/ -function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { - - return { - - getCount: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "GetCount")), - 'Failed to retrieve count'); - }, - - getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { - if (!filterContentTypes) { - filterContentTypes = []; - } - if (!filterPropertyTypes) { - filterPropertyTypes = []; - } - - var query = { - contentTypeId: contentTypeId, - filterContentTypes: filterContentTypes, - filterPropertyTypes: filterPropertyTypes - }; - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "GetAvailableCompositeMediaTypes"), - query), - 'Failed to retrieve data for content type id ' + contentTypeId); - }, - - /** + function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { + return { + getCount: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'GetCount')), 'Failed to retrieve count'); + }, + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + var query = { + contentTypeId: contentTypeId, + filterContentTypes: filterContentTypes, + filterPropertyTypes: filterPropertyTypes + }; + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'GetAvailableCompositeMediaTypes'), query), 'Failed to retrieve data for content type id ' + contentTypeId); + }, + /** * @ngdoc method * @name umbraco.resources.mediaTypeResource#getAllowedTypes * @methodOf umbraco.resources.mediaTypeResource @@ -3558,80 +2787,29 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * @returns {Promise} resourcePromise object. * */ - getAllowedTypes: function (mediaId) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "GetAllowedChildren", - [{ contentId: mediaId }])), - 'Failed to retrieve allowed types for media id ' + mediaId); - }, - - getById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve content type'); - }, - - getAll: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "GetAll")), - 'Failed to retrieve all content types'); - }, - - getScaffold: function (parentId) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "GetEmpty", { parentId: parentId })), - 'Failed to retrieve content type scaffold'); - }, - - deleteById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to retrieve content type'); - }, - - deleteContainerById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "DeleteContainer", - [{ id: id }])), - 'Failed to delete content type contaier'); - }, - - save: function (contentType) { - - var saveModel = umbDataFormatter.formatContentTypePostData(contentType); - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostSave"), saveModel), - 'Failed to save data for content type id ' + contentType.id); - }, - - /** + getAllowedTypes: function (mediaId) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'GetAllowedChildren', [{ contentId: mediaId }])), 'Failed to retrieve allowed types for media id ' + mediaId); + }, + getById: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'GetById', [{ id: id }])), 'Failed to retrieve content type'); + }, + getAll: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'GetAll')), 'Failed to retrieve all content types'); + }, + getScaffold: function (parentId) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'GetEmpty', { parentId: parentId })), 'Failed to retrieve content type scaffold'); + }, + deleteById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to retrieve content type'); + }, + deleteContainerById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'DeleteContainer', [{ id: id }])), 'Failed to delete content type contaier'); + }, + save: function (contentType) { + var saveModel = umbDataFormatter.formatContentTypePostData(contentType); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'PostSave'), saveModel), 'Failed to save data for content type id ' + contentType.id); + }, + /** * @ngdoc method * @name umbraco.resources.mediaTypeResource#move * @methodOf umbraco.resources.mediaTypeResource @@ -3654,162 +2832,125 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * @returns {Promise} resourcePromise object. * */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move content'); - }, - - copy: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; + move: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.id) { + throw 'args.id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'PostMove'), { + parentId: args.parentId, + id: args.id + }), 'Failed to move content'); + }, + copy: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.parentId) { + throw 'args.parentId cannot be null'; + } + if (!args.id) { + throw 'args.id cannot be null'; + } + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'PostCopy'), { + parentId: args.parentId, + id: args.id + }), 'Failed to copy content'); + }, + createContainer: function (parentId, name) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'PostCreateContainer', { + parentId: parentId, + name: name + })), 'Failed to create a folder under parent id ' + parentId); + }, + renameContainer: function (id, name) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'PostRenameContainer', { + id: id, + name: name + })), 'Failed to rename the folder with id ' + id); } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostCopy"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to copy content'); - }, - - createContainer: function(parentId, name) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "PostCreateContainer", - { parentId: parentId, name: name })), - 'Failed to create a folder under parent id ' + parentId); - } - - }; -} -angular.module('umbraco.resources').factory('mediaTypeResource', mediaTypeResource); - -/** + }; + } + angular.module('umbraco.resources').factory('mediaTypeResource', mediaTypeResource); + /** * @ngdoc service * @name umbraco.resources.memberResource * @description Loads in data for members **/ -function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { - - /** internal method process the saving of data and post processing the result */ - function saveMember(content, action, files) { - - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatMemberPostData(c, a); - } - }); - } - - return { - - getPagedResults: function (memberTypeAlias, options) { - - if (memberTypeAlias === 'all-members') { - memberTypeAlias = null; - } - - var defaults = { - pageSize: 25, - pageNumber: 1, - filter: '', - orderDirection: "Ascending", - orderBy: "LoginName", - orderBySystemField: true - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - //converts the value to a js bool - function toBool(v) { - if (angular.isNumber(v)) { - return v > 0; + function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { + /** internal method process the saving of data and post processing the result */ + function saveMember(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl('memberApiBaseUrl', 'PostSave'), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMemberPostData(c, a); + } + }); + } + return { + getPagedResults: function (memberTypeAlias, options) { + if (memberTypeAlias === 'all-members') { + memberTypeAlias = null; } - if (angular.isString(v)) { - return v === "true"; + var defaults = { + pageSize: 25, + pageNumber: 1, + filter: '', + orderDirection: 'Ascending', + orderBy: 'LoginName', + orderBySystemField: true + }; + if (options === undefined) { + options = {}; } - if (typeof v === "boolean") { - return v; + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === 'asc') { + options.orderDirection = 'Ascending'; + } else if (options.orderDirection === 'desc') { + options.orderDirection = 'Descending'; } - return false; - } - - var params = [ - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: toBool(options.orderBySystemField) }, - { filter: options.filter } - ]; - if (memberTypeAlias != null) { - params.push({ memberTypeAlias: memberTypeAlias }); - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetPagedResults", - params)), - 'Failed to retrieve member paged result'); - }, - - getListNode: function (listName) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetListNodeDisplay", - [{ listName: listName }])), - 'Failed to retrieve data for member list ' + listName); - }, - - /** + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === 'true'; + } + if (typeof v === 'boolean') { + return v; + } + return false; + } + var params = [ + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: toBool(options.orderBySystemField) }, + { filter: options.filter } + ]; + if (memberTypeAlias != null) { + params.push({ memberTypeAlias: memberTypeAlias }); + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberApiBaseUrl', 'GetPagedResults', params)), 'Failed to retrieve member paged result'); + }, + getListNode: function (listName) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberApiBaseUrl', 'GetListNodeDisplay', [{ listName: listName }])), 'Failed to retrieve data for member list ' + listName); + }, + /** * @ngdoc method * @name umbraco.resources.memberResource#getByKey * @methodOf umbraco.resources.memberResource @@ -3830,18 +2971,10 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the member item. * */ - getByKey: function (key) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetByKey", - [{ key: key }])), - 'Failed to retrieve data for member id ' + key); - }, - - /** + getByKey: function (key) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberApiBaseUrl', 'GetByKey', [{ key: key }])), 'Failed to retrieve data for member id ' + key); + }, + /** * @ngdoc method * @name umbraco.resources.memberResource#deleteByKey * @methodOf umbraco.resources.memberResource @@ -3861,17 +2994,10 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - deleteByKey: function (key) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "DeleteByKey", - [{ key: key }])), - 'Failed to delete item ' + key); - }, - - /** + deleteByKey: function (key) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('memberApiBaseUrl', 'DeleteByKey', [{ key: key }])), 'Failed to delete item ' + key); + }, + /** * @ngdoc method * @name umbraco.resources.memberResource#getScaffold * @methodOf umbraco.resources.memberResource @@ -3901,29 +3027,14 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the member scaffold. * */ - getScaffold: function (alias) { - - if (alias) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }])), - 'Failed to retrieve data for empty member item type ' + alias); - } - else { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty")), - 'Failed to retrieve data for empty member item type ' + alias); - } - - }, - - /** + getScaffold: function (alias) { + if (alias) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberApiBaseUrl', 'GetEmpty', [{ contentTypeAlias: alias }])), 'Failed to retrieve data for empty member item type ' + alias); + } else { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberApiBaseUrl', 'GetEmpty')), 'Failed to retrieve data for empty member item type ' + alias); + } + }, + /** * @ngdoc method * @name umbraco.resources.memberResource#save * @methodOf umbraco.resources.memberResource @@ -3951,101 +3062,58 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the saved media item. * */ - save: function (member, isNew, files) { - return saveMember(member, "save" + (isNew ? "New" : ""), files); - } - }; -} - -angular.module('umbraco.resources').factory('memberResource', memberResource); - -/** + save: function (member, isNew, files) { + return saveMember(member, 'save' + (isNew ? 'New' : ''), files); + } + }; + } + angular.module('umbraco.resources').factory('memberResource', memberResource); + /** * @ngdoc service * @name umbraco.resources.memberTypeResource * @description Loads in data for member types **/ -function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { - - return { - - getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { - if (!filterContentTypes) { - filterContentTypes = []; - } - if (!filterPropertyTypes) { - filterPropertyTypes = []; - } - - var query = ""; - _.each(filterContentTypes, function (item) { - query += "filterContentTypes=" + item + "&"; - }); - // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error - if (filterContentTypes.length === 0) { - query += "filterContentTypes=&"; - } - _.each(filterPropertyTypes, function (item) { - query += "filterPropertyTypes=" + item + "&"; - }); - // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error - if (filterPropertyTypes.length === 0) { - query += "filterPropertyTypes=&"; - } - query += "contentTypeId=" + contentTypeId; - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberTypeApiBaseUrl", - "GetAvailableCompositeMemberTypes", - query)), - 'Failed to retrieve data for content type id ' + contentTypeId); - }, - - //return all member types - getTypes: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberTypeApiBaseUrl", - "GetAllTypes")), - 'Failed to retrieve data for member types id'); - }, - - getById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberTypeApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve content type'); - }, - - deleteById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "memberTypeApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete member type'); - }, - - getScaffold: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberTypeApiBaseUrl", - "GetEmpty")), - 'Failed to retrieve content type scaffold'); - }, - - /** + function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { + return { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + var query = ''; + _.each(filterContentTypes, function (item) { + query += 'filterContentTypes=' + item + '&'; + }); + // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterContentTypes.length === 0) { + query += 'filterContentTypes=&'; + } + _.each(filterPropertyTypes, function (item) { + query += 'filterPropertyTypes=' + item + '&'; + }); + // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterPropertyTypes.length === 0) { + query += 'filterPropertyTypes=&'; + } + query += 'contentTypeId=' + contentTypeId; + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberTypeApiBaseUrl', 'GetAvailableCompositeMemberTypes', query)), 'Failed to retrieve data for content type id ' + contentTypeId); + }, + //return all member types + getTypes: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberTypeApiBaseUrl', 'GetAllTypes')), 'Failed to retrieve data for member types id'); + }, + getById: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberTypeApiBaseUrl', 'GetById', [{ id: id }])), 'Failed to retrieve content type'); + }, + deleteById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('memberTypeApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to delete member type'); + }, + getScaffold: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberTypeApiBaseUrl', 'GetEmpty')), 'Failed to retrieve content type scaffold'); + }, + /** * @ngdoc method * @name umbraco.resources.contentTypeResource#save * @methodOf umbraco.resources.contentTypeResource @@ -4057,97 +3125,70 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * @returns {Promise} resourcePromise object. * */ - save: function (contentType) { - - var saveModel = umbDataFormatter.formatContentTypePostData(contentType); - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostSave"), saveModel), - 'Failed to save data for member type id ' + contentType.id); - } - - }; -} -angular.module('umbraco.resources').factory('memberTypeResource', memberTypeResource); - -/** + save: function (contentType) { + var saveModel = umbDataFormatter.formatContentTypePostData(contentType); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('memberTypeApiBaseUrl', 'PostSave'), saveModel), 'Failed to save data for member type id ' + contentType.id); + } + }; + } + angular.module('umbraco.resources').factory('memberTypeResource', memberTypeResource); + angular.module('umbraco.resources').factory('Umbraco.PropertyEditors.NestedContent.Resources', function ($q, $http, umbRequestHelper) { + return { + getContentTypes: function () { + var url = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/backoffice/UmbracoApi/NestedContent/GetContentTypes'; + return umbRequestHelper.resourcePromise($http.get(url), 'Failed to retrieve content types'); + } + }; + }); + /** * @ngdoc service * @name umbraco.resources.ourPackageRepositoryResource * @description handles data for package installations **/ -function ourPackageRepositoryResource($q, $http, umbDataFormatter, umbRequestHelper) { - - var baseurl = Umbraco.Sys.ServerVariables.umbracoUrls.packagesRestApiBaseUrl; - - return { - - getDetails: function (packageId) { - - return umbRequestHelper.resourcePromise( - $http.get(baseurl + "/" + packageId + "?version=" + Umbraco.Sys.ServerVariables.application.version), - 'Failed to get package details'); - }, - - getCategories: function () { - - return umbRequestHelper.resourcePromise( - $http.get(baseurl), - 'Failed to query packages'); - }, - - getPopular: function (maxResults, category) { - - if (maxResults === undefined) { - maxResults = 10; - } - if (category === undefined) { - category = ""; - } - - return umbRequestHelper.resourcePromise( - $http.get(baseurl + "?pageIndex=0&pageSize=" + maxResults + "&category=" + category + "&order=Popular&version=" + Umbraco.Sys.ServerVariables.application.version), - 'Failed to query packages'); - }, - - search: function (pageIndex, pageSize, orderBy, category, query, canceler) { - - var httpConfig = {}; - if (canceler) { - httpConfig["timeout"] = canceler; - } - - if (category === undefined) { - category = ""; - } - if (query === undefined) { - query = ""; + function ourPackageRepositoryResource($q, $http, umbDataFormatter, umbRequestHelper) { + var baseurl = Umbraco.Sys.ServerVariables.umbracoUrls.packagesRestApiBaseUrl; + return { + getDetails: function (packageId) { + return umbRequestHelper.resourcePromise($http.get(baseurl + '/' + packageId + '?version=' + Umbraco.Sys.ServerVariables.application.version), 'Failed to get package details'); + }, + getCategories: function () { + return umbRequestHelper.resourcePromise($http.get(baseurl), 'Failed to query packages'); + }, + getPopular: function (maxResults, category) { + if (maxResults === undefined) { + maxResults = 10; + } + if (category === undefined) { + category = ''; + } + return umbRequestHelper.resourcePromise($http.get(baseurl + '?pageIndex=0&pageSize=' + maxResults + '&category=' + category + '&order=Popular&version=' + Umbraco.Sys.ServerVariables.application.version), 'Failed to query packages'); + }, + search: function (pageIndex, pageSize, orderBy, category, query, canceler) { + var httpConfig = {}; + if (canceler) { + httpConfig['timeout'] = canceler; + } + if (category === undefined) { + category = ''; + } + if (query === undefined) { + query = ''; + } + //order by score if there is nothing set + var order = !orderBy ? '&order=Default' : '&order=' + orderBy; + return umbRequestHelper.resourcePromise($http.get(baseurl + '?pageIndex=' + pageIndex + '&pageSize=' + pageSize + '&category=' + category + '&query=' + query + order + '&version=' + Umbraco.Sys.ServerVariables.application.version), httpConfig, 'Failed to query packages'); } - - //order by score if there is nothing set - var order = !orderBy ? "&order=Default" : ("&order=" + orderBy); - - return umbRequestHelper.resourcePromise( - $http.get(baseurl + "?pageIndex=" + pageIndex + "&pageSize=" + pageSize + "&category=" + category + "&query=" + query + order + "&version=" + Umbraco.Sys.ServerVariables.application.version), - httpConfig, - 'Failed to query packages'); - } - - - }; -} - -angular.module('umbraco.resources').factory('ourPackageRepositoryResource', ourPackageRepositoryResource); - -/** + }; + } + angular.module('umbraco.resources').factory('ourPackageRepositoryResource', ourPackageRepositoryResource); + /** * @ngdoc service * @name umbraco.resources.packageInstallResource * @description handles data for package installations **/ -function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { - - return { - - /** + function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { + return { + /** * @ngdoc method * @name umbraco.resources.packageInstallResource#getInstalled * @methodOf umbraco.resources.packageInstallResource @@ -4155,43 +3196,22 @@ function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { * @description * Gets a list of installed packages */ - getInstalled: function() { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "GetInstalled")), - 'Failed to get installed packages'); - }, - - validateInstalled: function (name, version) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "ValidateInstalled", { name: name, version: version })), - 'Failed to validate package ' + name); - }, - - deleteCreatedPackage: function (packageId) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "DeleteCreatedPackage", { packageId: packageId })), - 'Failed to delete package ' + packageId); - }, - - uninstall: function(packageId) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "Uninstall", { packageId: packageId })), - 'Failed to uninstall package'); - }, - - /** + getInstalled: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'GetInstalled')), 'Failed to get installed packages'); + }, + validateInstalled: function (name, version) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'ValidateInstalled', { + name: name, + version: version + })), 'Failed to validate package ' + name); + }, + deleteCreatedPackage: function (packageId) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'DeleteCreatedPackage', { packageId: packageId })), 'Failed to delete package ' + packageId); + }, + uninstall: function (packageId) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'Uninstall', { packageId: packageId })), 'Failed to uninstall package'); + }, + /** * @ngdoc method * @name umbraco.resources.packageInstallResource#fetchPackage * @methodOf umbraco.resources.packageInstallResource @@ -4210,18 +3230,11 @@ function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { * @param {String} the unique package ID * @returns {String} path to the downloaded zip file. * - */ - fetch: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "Fetch", - [{ packageGuid: id }])), - 'Failed to download package with guid ' + id); - }, - - /** + */ + fetch: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'Fetch', [{ packageGuid: id }])), 'Failed to download package with guid ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.packageInstallResource#createmanifest * @methodOf umbraco.resources.packageInstallResource @@ -4243,51 +3256,26 @@ function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { * @param {String} folder the path to the temporary folder containing files * @returns {Int} the ID assigned to the saved package manifest * - */ - import: function (package) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "Import"), package), - 'Failed to install package. Error during the step "Import" '); - }, - - installFiles: function (package) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "InstallFiles"), package), - 'Failed to install package. Error during the step "InstallFiles" '); - }, - - installData: function (package) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "InstallData"), package), - 'Failed to install package. Error during the step "InstallData" '); - }, - - cleanUp: function (package) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "CleanUp"), package), - 'Failed to install package. Error during the step "CleanUp" '); - } - }; -} - -angular.module('umbraco.resources').factory('packageResource', packageResource); - -/** + */ + import: function (package) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'Import'), package), 'Failed to install package. Error during the step "Import" '); + }, + installFiles: function (package) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'InstallFiles'), package), 'Failed to install package. Error during the step "InstallFiles" '); + }, + checkRestart: function (package) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'CheckRestart'), package), 'Failed to install package. Error during the step "CheckRestart" '); + }, + installData: function (package) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'InstallData'), package), 'Failed to install package. Error during the step "InstallData" '); + }, + cleanUp: function (package) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'CleanUp'), package), 'Failed to install package. Error during the step "CleanUp" '); + } + }; + } + angular.module('umbraco.resources').factory('packageResource', packageResource); + /** * @ngdoc service * @name umbraco.resources.redirectUrlResource * @function @@ -4295,12 +3283,10 @@ angular.module('umbraco.resources').factory('packageResource', packageResource); * @description * Used by the redirect url dashboard to get urls and send requests to remove redirects. */ -(function() { - 'use strict'; - - function redirectUrlsResource($http, umbRequestHelper) { - - /** + (function () { + 'use strict'; + function redirectUrlsResource($http, umbRequestHelper) { + /** * @ngdoc function * @name umbraco.resources.redirectUrlResource#searchRedirectUrls * @methodOf umbraco.resources.redirectUrlResource @@ -4319,28 +3305,17 @@ angular.module('umbraco.resources').factory('packageResource', packageResource); * @param {Int} pageIndex index of the page to retrive items from * @param {Int} pageSize The number of items on a page */ - function searchRedirectUrls(searchTerm, pageIndex, pageSize) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "redirectUrlManagementApiBaseUrl", - "SearchRedirectUrls", - { searchTerm: searchTerm, page: pageIndex, pageSize: pageSize })), - 'Failed to retrieve data for searching redirect urls'); - } - - function getEnableState() { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "redirectUrlManagementApiBaseUrl", - "GetEnableState")), - 'Failed to retrieve data to check if the 301 redirect is enabled'); - } - - /** + function searchRedirectUrls(searchTerm, pageIndex, pageSize) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('redirectUrlManagementApiBaseUrl', 'SearchRedirectUrls', { + searchTerm: searchTerm, + page: pageIndex, + pageSize: pageSize + })), 'Failed to retrieve data for searching redirect urls'); + } + function getEnableState() { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('redirectUrlManagementApiBaseUrl', 'GetEnableState')), 'Failed to retrieve data to check if the 301 redirect is enabled'); + } + /** * @ngdoc function * @name umbraco.resources.redirectUrlResource#deleteRedirectUrl * @methodOf umbraco.resources.redirectUrlResource @@ -4357,16 +3332,10 @@ angular.module('umbraco.resources').factory('packageResource', packageResource); * * @param {Int} id Id of the redirect */ - function deleteRedirectUrl(id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "redirectUrlManagementApiBaseUrl", - "DeleteRedirectUrl", { id: id })), - 'Failed to remove redirect'); - } - - /** + function deleteRedirectUrl(id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('redirectUrlManagementApiBaseUrl', 'DeleteRedirectUrl', { id: id })), 'Failed to remove redirect'); + } + /** * @ngdoc function * @name umbraco.resources.redirectUrlResource#toggleUrlTracker * @methodOf umbraco.resources.redirectUrlResource @@ -4383,39 +3352,27 @@ angular.module('umbraco.resources').factory('packageResource', packageResource); * * @param {Bool} disable true/false to disable/enable the url tracker */ - function toggleUrlTracker(disable) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "redirectUrlManagementApiBaseUrl", - "ToggleUrlTracker", { disable: disable })), - 'Failed to toggle redirect url tracker'); + function toggleUrlTracker(disable) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('redirectUrlManagementApiBaseUrl', 'ToggleUrlTracker', { disable: disable })), 'Failed to toggle redirect url tracker'); + } + var resource = { + searchRedirectUrls: searchRedirectUrls, + deleteRedirectUrl: deleteRedirectUrl, + toggleUrlTracker: toggleUrlTracker, + getEnableState: getEnableState + }; + return resource; } - - var resource = { - searchRedirectUrls: searchRedirectUrls, - deleteRedirectUrl: deleteRedirectUrl, - toggleUrlTracker: toggleUrlTracker, - getEnableState: getEnableState - }; - - return resource; - - } - - angular.module('umbraco.resources').factory('redirectUrlsResource', redirectUrlsResource); - -})(); - -/** + angular.module('umbraco.resources').factory('redirectUrlsResource', redirectUrlsResource); + }()); + /** * @ngdoc service * @name umbraco.resources.relationResource * @description Handles loading of relation data **/ -function relationResource($q, $http, umbRequestHelper) { - return { - - /** + function relationResource($q, $http, umbRequestHelper) { + return { + /** * @ngdoc method * @name umbraco.resources.relationResource#getByChildId * @methodOf umbraco.resources.relationResource @@ -4428,18 +3385,13 @@ function relationResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the relations array. * */ - getByChildId: function (id, alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "relationApiBaseUrl", - "GetByChildId", - [{ childId: id, relationTypeAlias: alias }])), - "Failed to get relation by child ID " + id + " and type of " + alias); - }, - - /** + getByChildId: function (id, alias) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('relationApiBaseUrl', 'GetByChildId', [{ + childId: id, + relationTypeAlias: alias + }])), 'Failed to get relation by child ID ' + id + ' and type of ' + alias); + }, + /** * @ngdoc method * @name umbraco.resources.relationResource#deleteById * @methodOf umbraco.resources.relationResource @@ -4459,62 +3411,46 @@ function relationResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - deleteById: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "relationApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - } - }; -} - -angular.module('umbraco.resources').factory('relationResource', relationResource); -/** + deleteById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('relationApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to delete item ' + id); + } + }; + } + angular.module('umbraco.resources').factory('relationResource', relationResource); + /** * @ngdoc service * @name umbraco.resources.sectionResource * @description Loads in data for section **/ -function sectionResource($q, $http, umbRequestHelper) { - - /** internal method to get the tree app url */ - function getSectionsUrl(section) { - return Umbraco.Sys.ServerVariables.sectionApiBaseUrl + "GetSections"; + function sectionResource($q, $http, umbRequestHelper) { + /** internal method to get the tree app url */ + function getSectionsUrl(section) { + return Umbraco.Sys.ServerVariables.sectionApiBaseUrl + 'GetSections'; + } + //the factory object returned + return { + /** Loads in the data to display the section list */ + getSections: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('sectionApiBaseUrl', 'GetSections')), 'Failed to retrieve data for sections'); + }, + /** Loads in all available sections */ + getAllSections: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('sectionApiBaseUrl', 'GetAllSections')), 'Failed to retrieve data for sections'); + } + }; } - - //the factory object returned - return { - /** Loads in the data to display the section list */ - getSections: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "sectionApiBaseUrl", - "GetSections")), - 'Failed to retrieve data for sections'); - - } - }; -} - -angular.module('umbraco.resources').factory('sectionResource', sectionResource); - -/** + angular.module('umbraco.resources').factory('sectionResource', sectionResource); + /** * @ngdoc service * @name umbraco.resources.stylesheetResource * @description service to retrieve available stylesheets * * **/ -function stylesheetResource($q, $http, umbRequestHelper) { - - //the factory object returned - return { - - /** + function stylesheetResource($q, $http, umbRequestHelper) { + //the factory object returned + return { + /** * @ngdoc method * @name umbraco.resources.stylesheetResource#getAll * @methodOf umbraco.resources.stylesheetResource @@ -4533,16 +3469,10 @@ function stylesheetResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the stylesheets. * */ - getAll: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "stylesheetApiBaseUrl", - "GetAll")), - 'Failed to retrieve stylesheets '); - }, - - /** + getAll: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('stylesheetApiBaseUrl', 'GetAll')), 'Failed to retrieve stylesheets '); + }, + /** * @ngdoc method * @name umbraco.resources.stylesheetResource#getRulesByName * @methodOf umbraco.resources.stylesheetResource @@ -4561,30 +3491,20 @@ function stylesheetResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the rules. * */ - getRulesByName: function (name) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "stylesheetApiBaseUrl", - "GetRulesByName", - [{ name: name }])), - 'Failed to retrieve stylesheets '); - } - }; -} - -angular.module('umbraco.resources').factory('stylesheetResource', stylesheetResource); - -/** + getRulesByName: function (name) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('stylesheetApiBaseUrl', 'GetRulesByName', [{ name: name }])), 'Failed to retrieve stylesheets '); + } + }; + } + angular.module('umbraco.resources').factory('stylesheetResource', stylesheetResource); + /** * @ngdoc service * @name umbraco.resources.templateResource * @description Loads in data for templates **/ -function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { - - return { - - /** + function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { + return { + /** * @ngdoc method * @name umbraco.resources.templateResource#getById * @methodOf umbraco.resources.templateResource @@ -4604,18 +3524,10 @@ function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "templateApiBaseUrl", - "GetById", - [{ id: id }])), - "Failed to retrieve data for template id " + id); - }, - - /** + getById: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('templateApiBaseUrl', 'GetById', [{ id: id }])), 'Failed to retrieve data for template id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.templateResource#getByAlias * @methodOf umbraco.resources.templateResource @@ -4635,18 +3547,10 @@ function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getByAlias: function (alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "templateApiBaseUrl", - "GetByAlias", - [{ alias: alias }])), - "Failed to retrieve data for template with alias: " + alias); - }, - - /** + getByAlias: function (alias) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('templateApiBaseUrl', 'GetByAlias', [{ alias: alias }])), 'Failed to retrieve data for template with alias: ' + alias); + }, + /** * @ngdoc method * @name umbraco.resources.templateResource#getAll * @methodOf umbraco.resources.templateResource @@ -4665,18 +3569,10 @@ function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - getAll: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "templateApiBaseUrl", - "GetAll")), - "Failed to retrieve data"); - }, - - - /** + getAll: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('templateApiBaseUrl', 'GetAll')), 'Failed to retrieve data'); + }, + /** * @ngdoc method * @name umbraco.resources.templateResource#getScaffold * @methodOf umbraco.resources.templateResource @@ -4697,18 +3593,10 @@ function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the template scaffold. * */ - getScaffold: function (id) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "templateApiBaseUrl", - "GetScaffold", - [{ id: id }] )), - "Failed to retrieve data for empty template"); - }, - - /** + getScaffold: function (id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('templateApiBaseUrl', 'GetScaffold', [{ id: id }])), 'Failed to retrieve data for empty template'); + }, + /** * @ngdoc method * @name umbraco.resources.templateResource#deleteById * @methodOf umbraco.resources.templateResource @@ -4728,17 +3616,10 @@ function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - deleteById: function(id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "templateApiBaseUrl", - "DeleteById", - [{ id: id }])), - "Failed to delete item " + id); - }, - - /** + deleteById: function (id) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('templateApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to delete item ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.templateResource#save * @methodOf umbraco.resources.templateResource @@ -4758,21 +3639,13 @@ function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object. * */ - save: function (template) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "templateApiBaseUrl", - "PostSave"), - template), - "Failed to save data for template id " + template.id); - } - }; -} - -angular.module("umbraco.resources").factory("templateResource", templateResource); - -/** + save: function (template) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('templateApiBaseUrl', 'PostSave'), template), 'Failed to save data for template id ' + template.id); + } + }; + } + angular.module('umbraco.resources').factory('templateResource', templateResource); + /** * @ngdoc service * @name umbraco.resources.templateQueryResource * @function @@ -4780,12 +3653,10 @@ angular.module("umbraco.resources").factory("templateResource", templateResource * @description * Used by the query builder */ -(function () { - 'use strict'; - - function templateQueryResource($http, umbRequestHelper) { - - /** + (function () { + 'use strict'; + function templateQueryResource($http, umbRequestHelper) { + /** * @ngdoc function * @name umbraco.resources.templateQueryResource#getAllowedProperties * @methodOf umbraco.resources.templateQueryResource @@ -4801,16 +3672,10 @@ angular.module("umbraco.resources").factory("templateResource", templateResource * }); * */ - function getAllowedProperties() { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "templateQueryApiBaseUrl", - "GetAllowedProperties")), - 'Failed to retrieve properties'); - } - - /** + function getAllowedProperties() { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('templateQueryApiBaseUrl', 'GetAllowedProperties')), 'Failed to retrieve properties'); + } + /** * @ngdoc function * @name umbraco.resources.templateQueryResource#getContentTypes * @methodOf umbraco.resources.templateQueryResource @@ -4826,16 +3691,10 @@ angular.module("umbraco.resources").factory("templateResource", templateResource * }); * */ - function getContentTypes() { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "templateQueryApiBaseUrl", - "GetContentTypes")), - 'Failed to retrieve content types'); - } - - /** + function getContentTypes() { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('templateQueryApiBaseUrl', 'GetContentTypes')), 'Failed to retrieve content types'); + } + /** * @ngdoc function * @name umbraco.resources.templateQueryResource#getFilterConditions * @methodOf umbraco.resources.templateQueryResource @@ -4851,16 +3710,10 @@ angular.module("umbraco.resources").factory("templateResource", templateResource * }); * */ - function getFilterConditions() { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "templateQueryApiBaseUrl", - "GetFilterConditions")), - 'Failed to retrieve filter conditions'); - } - - /** + function getFilterConditions() { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('templateQueryApiBaseUrl', 'GetFilterConditions')), 'Failed to retrieve filter conditions'); + } + /** * @ngdoc function * @name umbraco.resources.templateQueryResource#postTemplateQuery * @methodOf umbraco.resources.templateQueryResource @@ -4899,148 +3752,469 @@ angular.module("umbraco.resources").factory("templateResource", templateResource * * @param {object} query Query to build result */ - function postTemplateQuery(query) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "templateQueryApiBaseUrl", - "PostTemplateQuery"), - query), - 'Failed to retrieve query'); + function postTemplateQuery(query) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('templateQueryApiBaseUrl', 'PostTemplateQuery'), query), 'Failed to retrieve query'); + } + var resource = { + getAllowedProperties: getAllowedProperties, + getContentTypes: getContentTypes, + getFilterConditions: getFilterConditions, + postTemplateQuery: postTemplateQuery + }; + return resource; } - - var resource = { - getAllowedProperties: getAllowedProperties, - getContentTypes: getContentTypes, - getFilterConditions: getFilterConditions, - postTemplateQuery: postTemplateQuery - }; - - return resource; - - } - - angular.module('umbraco.resources').factory('templateQueryResource', templateQueryResource); - -})(); - -/** + angular.module('umbraco.resources').factory('templateQueryResource', templateQueryResource); + }()); + /** * @ngdoc service * @name umbraco.resources.treeResource * @description Loads in data for trees **/ -function treeResource($q, $http, umbRequestHelper) { - - /** internal method to get the tree node's children url */ - function getTreeNodesUrl(node) { - if (!node.childNodesUrl) { - throw "No childNodesUrl property found on the tree node, cannot load child nodes"; + function treeResource($q, $http, umbRequestHelper) { + /** internal method to get the tree node's children url */ + function getTreeNodesUrl(node) { + if (!node.childNodesUrl) { + throw 'No childNodesUrl property found on the tree node, cannot load child nodes'; + } + return node.childNodesUrl; } - return node.childNodesUrl; - } - - /** internal method to get the tree menu url */ - function getTreeMenuUrl(node) { - if (!node.menuUrl) { - return null; + /** internal method to get the tree menu url */ + function getTreeMenuUrl(node) { + if (!node.menuUrl) { + return null; + } + return node.menuUrl; } - return node.menuUrl; + //the factory object returned + return { + /** Loads in the data to display the nodes menu */ + loadMenu: function (node) { + var treeMenuUrl = getTreeMenuUrl(node); + if (treeMenuUrl !== undefined && treeMenuUrl !== null && treeMenuUrl.length > 0) { + return umbRequestHelper.resourcePromise($http.get(getTreeMenuUrl(node)), 'Failed to retrieve data for a node\'s menu ' + node.id); + } else { + return $q.reject({ errorMsg: 'No tree menu url defined for node ' + node.id }); + } + }, + /** Loads in the data to display the nodes for an application */ + loadApplication: function (options) { + if (!options || !options.section) { + throw 'The object specified for does not contain a \'section\' property'; + } + if (!options.tree) { + options.tree = ''; + } + if (!options.isDialog) { + options.isDialog = false; + } + //create the query string for the tree request, these are the mandatory options: + var query = 'application=' + options.section + '&tree=' + options.tree + '&isDialog=' + options.isDialog; + //if you need to load a not initialized tree set this value to false - default is true + if (options.onlyinitialized) { + query += '&onlyInitialized=' + options.onlyinitialized; + } + //the options can contain extra query string parameters + if (options.queryString) { + query += '&' + options.queryString; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('treeApplicationApiBaseUrl', 'GetApplicationTrees', query)), 'Failed to retrieve data for application tree ' + options.section); + }, + /** Loads in the data to display the child nodes for a given node */ + loadNodes: function (options) { + if (!options || !options.node) { + throw 'The options parameter object does not contain the required properties: \'node\''; + } + return umbRequestHelper.resourcePromise($http.get(getTreeNodesUrl(options.node)), 'Failed to retrieve data for child nodes ' + options.node.nodeId); + } + }; } - - //the factory object returned - return { - - /** Loads in the data to display the nodes menu */ - loadMenu: function (node) { - var treeMenuUrl = getTreeMenuUrl(node); - if (treeMenuUrl !== undefined && treeMenuUrl !== null && treeMenuUrl.length > 0) { - return umbRequestHelper.resourcePromise( - $http.get(getTreeMenuUrl(node)), - "Failed to retrieve data for a node's menu " + node.id); - } else { - return $q.reject({ - errorMsg: "No tree menu url defined for node " + node.id - }); + angular.module('umbraco.resources').factory('treeResource', treeResource); + /** + * @ngdoc service + * @name umbraco.resources.usersResource + * @function + * + * @description + * Used by the users section to get users and send requests to create, invite, delete, etc. users. + */ + (function () { + 'use strict'; + function userGroupsResource($http, umbRequestHelper, $q, umbDataFormatter) { + function getUserGroupScaffold() { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('userGroupsApiBaseUrl', 'GetEmptyUserGroup')), 'Failed to get the user group scaffold'); + } + function saveUserGroup(userGroup, isNew) { + if (!userGroup) { + throw 'userGroup not specified'; + } + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserGroupPostData(userGroup, 'save' + (isNew ? 'New' : '')); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userGroupsApiBaseUrl', 'PostSaveUserGroup'), formattedSaveData), 'Failed to save user group'); } - }, - - /** Loads in the data to display the nodes for an application */ - loadApplication: function (options) { - - if (!options || !options.section) { - throw "The object specified for does not contain a 'section' property"; + function getUserGroup(id) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('userGroupsApiBaseUrl', 'GetUserGroup', { id: id })), 'Failed to retrieve data for user group ' + id); } - - if(!options.tree){ - options.tree = ""; + function getUserGroups(args) { + if (!args) { + args = { onlyCurrentUserGroups: true }; + } + if (args.onlyCurrentUserGroups === undefined || args.onlyCurrentUserGroups === null) { + args.onlyCurrentUserGroups = true; + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('userGroupsApiBaseUrl', 'GetUserGroups', args)), 'Failed to retrieve user groups'); } - if (!options.isDialog) { - options.isDialog = false; + function deleteUserGroups(userGroupIds) { + var query = 'userGroupIds=' + userGroupIds.join('&userGroupIds='); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userGroupsApiBaseUrl', 'PostDeleteUserGroups', query)), 'Failed to delete user groups'); } - - //create the query string for the tree request, these are the mandatory options: - var query = "application=" + options.section + "&tree=" + options.tree + "&isDialog=" + options.isDialog; - - //if you need to load a not initialized tree set this value to false - default is true - if (options.onlyinitialized) { - query += "&onlyInitialized=" + options.onlyinitialized; + var resource = { + saveUserGroup: saveUserGroup, + getUserGroup: getUserGroup, + getUserGroups: getUserGroups, + getUserGroupScaffold: getUserGroupScaffold, + deleteUserGroups: deleteUserGroups + }; + return resource; + } + angular.module('umbraco.resources').factory('userGroupsResource', userGroupsResource); + }()); + /** + * @ngdoc service + * @name umbraco.resources.usersResource + * @function + * + * @description + * Used by the users section to get users and send requests to create, invite, disable, etc. users. + */ + (function () { + 'use strict'; + function usersResource($http, umbRequestHelper, $q, umbDataFormatter) { + /** + * @ngdoc method + * @name umbraco.resources.usersResource#clearAvatar + * @methodOf umbraco.resources.usersResource + * + * @description + * Deletes the user avatar + * + * ##usage + *
    +          * usersResource.clearAvatar(1)
    +          *    .then(function() {
    +          *        alert("avatar is gone");
    +          *    });
    +          * 
    + * + * @param {Array} id id of user. + * @returns {Promise} resourcePromise object. + * + */ + function clearAvatar(userId) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostClearAvatar', { id: userId })), 'Failed to clear the user avatar ' + userId); } - - //the options can contain extra query string parameters - if (options.queryString) { - query += "&" + options.queryString; + /** + * @ngdoc method + * @name umbraco.resources.usersResource#disableUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Disables a collection of users + * + * ##usage + *
    +          * usersResource.disableUsers([1, 2, 3, 4, 5])
    +          *    .then(function() {
    +          *        alert("users were disabled");
    +          *    });
    +          * 
    + * + * @param {Array} ids ids of users to disable. + * @returns {Promise} resourcePromise object. + * + */ + function disableUsers(userIds) { + if (!userIds) { + throw 'userIds not specified'; + } + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = 'userIds=' + userIds.join('&userIds='); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostDisableUsers', qry)), 'Failed to disable the users ' + userIds.join(',')); } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "treeApplicationApiBaseUrl", - "GetApplicationTrees", - query)), - 'Failed to retrieve data for application tree ' + options.section); - }, - - /** Loads in the data to display the child nodes for a given node */ - loadNodes: function (options) { - - if (!options || !options.node) { - throw "The options parameter object does not contain the required properties: 'node'"; + /** + * @ngdoc method + * @name umbraco.resources.usersResource#enableUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Enables a collection of users + * + * ##usage + *
    +          * usersResource.enableUsers([1, 2, 3, 4, 5])
    +          *    .then(function() {
    +          *        alert("users were enabled");
    +          *    });
    +          * 
    + * + * @param {Array} ids ids of users to enable. + * @returns {Promise} resourcePromise object. + * + */ + function enableUsers(userIds) { + if (!userIds) { + throw 'userIds not specified'; + } + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = 'userIds=' + userIds.join('&userIds='); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostEnableUsers', qry)), 'Failed to enable the users ' + userIds.join(',')); } - - return umbRequestHelper.resourcePromise( - $http.get(getTreeNodesUrl(options.node)), - 'Failed to retrieve data for child nodes ' + options.node.nodeId); - } - }; -} - -angular.module('umbraco.resources').factory('treeResource', treeResource); - -/** - * @ngdoc service - * @name umbraco.resources.userResource - **/ -function userResource($q, $http, umbDataFormatter, umbRequestHelper) { - - return { - - disableUser: function (userId) { - - if (!userId) { - throw "userId not specified"; + /** + * @ngdoc method + * @name umbraco.resources.usersResource#unlockUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Unlocks a collection of users + * + * ##usage + *
    +          * usersResource.unlockUsers([1, 2, 3, 4, 5])
    +          *    .then(function() {
    +          *        alert("users were unlocked");
    +          *    });
    +          * 
    + * + * @param {Array} ids ids of users to unlock. + * @returns {Promise} resourcePromise object. + * + */ + function unlockUsers(userIds) { + if (!userIds) { + throw 'userIds not specified'; + } + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = 'userIds=' + userIds.join('&userIds='); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostUnlockUsers', qry)), 'Failed to enable the users ' + userIds.join(',')); } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "PostDisableUser", [{ userId: userId }])), - 'Failed to disable the user ' + userId); + /** + * @ngdoc method + * @name umbraco.resources.usersResource#setUserGroupsOnUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Overwrites the existing user groups on a collection of users + * + * ##usage + *
    +          * usersResource.setUserGroupsOnUsers(['admin', 'editor'], [1, 2, 3, 4, 5])
    +          *    .then(function() {
    +          *        alert("users were updated");
    +          *    });
    +          * 
    + * + * @param {Array} userGroupAliases aliases of user groups. + * @param {Array} ids ids of users to update. + * @returns {Promise} resourcePromise object. + * + */ + function setUserGroupsOnUsers(userGroups, userIds) { + var userGroupAliases = userGroups.map(function (o) { + return o.alias; + }); + var query = 'userGroupAliases=' + userGroupAliases.join('&userGroupAliases=') + '&userIds=' + userIds.join('&userIds='); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostSetUserGroupsOnUsers', query)), 'Failed to set user groups ' + userGroupAliases.join(',') + ' on the users ' + userIds.join(',')); + } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#getPagedResults + * @methodOf umbraco.resources.usersResource + * + * @description + * Get users + * + * ##usage + *
    +          * usersResource.getPagedResults({pageSize: 10, pageNumber: 2})
    +          *    .then(function(data) {
    +          *        var users = data.items;
    +          *        alert('they are here!');
    +          *    });
    +          * 
    + * + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of users per page, default = 25 + * @param {Int} options.pageNumber if paging data, current page index, default = 1 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order users by, default: `Username` + * @param {Array} options.userGroups property to filter users by user group + * @param {Array} options.userStates property to filter users by user state + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + function getPagedResults(options) { + var defaults = { + pageSize: 25, + pageNumber: 1, + filter: '', + orderDirection: 'Ascending', + orderBy: 'Username', + userGroups: [], + userStates: [] + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === 'asc') { + options.orderDirection = 'Ascending'; + } else if (options.orderDirection === 'desc') { + options.orderDirection = 'Descending'; + } + var params = { + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection, + filter: options.filter + }; + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = umbRequestHelper.dictionaryToQueryString(params); + if (options.userGroups.length > 0) { + //we need to create a custom query string for an array + qry += '&userGroups=' + options.userGroups.join('&userGroups='); + } + if (options.userStates.length > 0) { + //we need to create a custom query string for an array + qry += '&userStates=' + options.userStates.join('&userStates='); + } + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('userApiBaseUrl', 'GetPagedUsers', qry)), 'Failed to retrieve users paged result'); + } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#getUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Gets a user + * + * ##usage + *
    +          * usersResource.getUser(1)
    +          *    .then(function(user) {
    +          *        alert("It's here");
    +          *    });
    +          * 
    + * + * @param {Array} id user id. + * @returns {Promise} resourcePromise object containing the user. + * + */ + function getUser(userId) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('userApiBaseUrl', 'GetById', { id: userId })), 'Failed to retrieve data for user ' + userId); + } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#createUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Creates a new user + * + * ##usage + *
    +          * usersResource.createUser(user)
    +          *    .then(function(newUser) {
    +          *        alert("It's here");
    +          *    });
    +          * 
    + * + * @param {Object} user user to create + * @returns {Promise} resourcePromise object containing the new user. + * + */ + function createUser(user) { + if (!user) { + throw 'user not specified'; + } + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserPostData(user); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostCreateUser'), formattedSaveData), 'Failed to save user'); + } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#inviteUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Creates and sends an email invitation to a new user + * + * ##usage + *
    +          * usersResource.inviteUser(user)
    +          *    .then(function(newUser) {
    +          *        alert("It's here");
    +          *    });
    +          * 
    + * + * @param {Object} user user to invite + * @returns {Promise} resourcePromise object containing the new user. + * + */ + function inviteUser(user) { + if (!user) { + throw 'user not specified'; + } + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserPostData(user); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostInviteUser'), formattedSaveData), 'Failed to invite user'); + } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#saveUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Saves a user + * + * ##usage + *
    +          * usersResource.saveUser(user)
    +          *    .then(function(updatedUser) {
    +          *        alert("It's here");
    +          *    });
    +          * 
    + * + * @param {Object} user object to save + * @returns {Promise} resourcePromise object containing the updated user. + * + */ + function saveUser(user) { + if (!user) { + throw 'user not specified'; + } + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserPostData(user); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostSaveUser'), formattedSaveData), 'Failed to save user'); + } + var resource = { + disableUsers: disableUsers, + enableUsers: enableUsers, + unlockUsers: unlockUsers, + setUserGroupsOnUsers: setUserGroupsOnUsers, + getPagedResults: getPagedResults, + getUser: getUser, + createUser: createUser, + inviteUser: inviteUser, + saveUser: saveUser, + clearAvatar: clearAvatar + }; + return resource; } - }; -} - -angular.module('umbraco.resources').factory('userResource', userResource); - - -})(); \ No newline at end of file + angular.module('umbraco.resources').factory('usersResource', usersResource); + }()); +}()); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.security.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.security.js index de4fa440..659d5cbd 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.security.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.security.js @@ -1,212 +1,197 @@ -/*! umbraco - * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2017 Umbraco HQ; - * Licensed - */ - -(function() { - -//TODO: This is silly and unecessary to have a separate module for this -angular.module('umbraco.security.retryQueue', []); -angular.module('umbraco.security.interceptor', ['umbraco.security.retryQueue']); -angular.module('umbraco.security', ['umbraco.security.retryQueue', 'umbraco.security.interceptor']); -//TODO: This is silly and unecessary to have a separate module for this -angular.module('umbraco.security.retryQueue', []) - -// This is a generic retry queue for security failures. Each item is expected to expose two functions: retry and cancel. -.factory('securityRetryQueue', ['$q', '$log', function ($q, $log) { - - var retryQueue = []; - var retryUser = null; - - var service = { - // The security service puts its own handler in here! - onItemAddedCallbacks: [], - - hasMore: function() { - return retryQueue.length > 0; - }, - push: function(retryItem) { - retryQueue.push(retryItem); - // Call all the onItemAdded callbacks - angular.forEach(service.onItemAddedCallbacks, function(cb) { - try { - cb(retryItem); - } catch(e) { - $log.error('securityRetryQueue.push(retryItem): callback threw an error' + e); - } - }); - }, - pushRetryFn: function(reason, userName, retryFn) { - // The reason parameter is optional - if ( arguments.length === 2) { - retryFn = userName; - userName = reason; - reason = undefined; - } - - if ((retryUser && retryUser !== userName) || userName === null) { - throw new Error('invalid user'); - } - - retryUser = userName; - - // The deferred object that will be resolved or rejected by calling retry or cancel - var deferred = $q.defer(); - var retryItem = { - reason: reason, - retry: function() { - // Wrap the result of the retryFn into a promise if it is not already - $q.when(retryFn()).then(function(value) { - // If it was successful then resolve our deferred - deferred.resolve(value); - }, function(value) { - // Othewise reject it - deferred.reject(value); - }); - }, - cancel: function() { - // Give up on retrying and reject our deferred - deferred.reject(); +(function () { + angular.module('umbraco.security.retryQueue', []); + angular.module('umbraco.security.interceptor', ['umbraco.security.retryQueue']); + angular.module('umbraco.security', [ + 'umbraco.security.retryQueue', + 'umbraco.security.interceptor' + ]); + //TODO: This is silly and unecessary to have a separate module for this + angular.module('umbraco.security.retryQueue', []) // This is a generic retry queue for security failures. Each item is expected to expose two functions: retry and cancel. +.factory('securityRetryQueue', [ + '$q', + '$log', + function ($q, $log) { + var retryQueue = []; + var retryUser = null; + var service = { + // The security service puts its own handler in here! + onItemAddedCallbacks: [], + hasMore: function () { + return retryQueue.length > 0; + }, + push: function (retryItem) { + retryQueue.push(retryItem); + // Call all the onItemAdded callbacks + angular.forEach(service.onItemAddedCallbacks, function (cb) { + try { + cb(retryItem); + } catch (e) { + $log.error('securityRetryQueue.push(retryItem): callback threw an error' + e); + } + }); + }, + pushRetryFn: function (reason, userName, retryFn) { + // The reason parameter is optional + if (arguments.length === 2) { + retryFn = userName; + userName = reason; + reason = undefined; + } + if (retryUser && retryUser !== userName || userName === null) { + throw new Error('invalid user'); + } + retryUser = userName; + // The deferred object that will be resolved or rejected by calling retry or cancel + var deferred = $q.defer(); + var retryItem = { + reason: reason, + retry: function () { + // Wrap the result of the retryFn into a promise if it is not already + $q.when(retryFn()).then(function (value) { + // If it was successful then resolve our deferred + deferred.resolve(value); + }, function (value) { + // Othewise reject it + deferred.reject(value); + }); + }, + cancel: function () { + // Give up on retrying and reject our deferred + deferred.reject(); + } + }; + service.push(retryItem); + return deferred.promise; + }, + retryReason: function () { + return service.hasMore() && retryQueue[0].reason; + }, + cancelAll: function () { + while (service.hasMore()) { + retryQueue.shift().cancel(); + } + retryUser = null; + }, + retryAll: function (userName) { + if (retryUser == null) { + return; + } + if (retryUser !== userName) { + service.cancelAll(); + return; + } + while (service.hasMore()) { + retryQueue.shift().retry(); + } + } + }; + return service; } - }; - service.push(retryItem); - return deferred.promise; - }, - retryReason: function() { - return service.hasMore() && retryQueue[0].reason; - }, - cancelAll: function() { - while(service.hasMore()) { - retryQueue.shift().cancel(); - } - retryUser = null; - }, - retryAll: function (userName) { - - if (retryUser == null) { - return; - } - - if (retryUser !== userName) { - service.cancelAll(); - return; - } - - while(service.hasMore()) { - retryQueue.shift().retry(); - } - } - }; - return service; -}]); -angular.module('umbraco.security.interceptor') - // This http interceptor listens for authentication successes and failures - .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', 'requestInterceptorFilter', function ($injector, queue, notifications, requestInterceptorFilter) { - return function(promise) { - - return promise.then( - function(originalResponse) { + ]); + angular.module('umbraco.security.interceptor') // This http interceptor listens for authentication successes and failures +.factory('securityInterceptor', [ + '$injector', + 'securityRetryQueue', + 'notificationsService', + 'eventsService', + 'requestInterceptorFilter', + function ($injector, queue, notifications, eventsService, requestInterceptorFilter) { + return function (promise) { + return promise.then(function (originalResponse) { // Intercept successful requests - //Here we'll check if our custom header is in the response which indicates how many seconds the user's session has before it //expires. Then we'll update the user in the user service accordingly. var headers = originalResponse.headers(); - if (headers["x-umb-user-seconds"]) { + if (headers['x-umb-user-seconds']) { // We must use $injector to get the $http service to prevent circular dependency var userService = $injector.get('userService'); - userService.setUserTimeout(headers["x-umb-user-seconds"]); + userService.setUserTimeout(headers['x-umb-user-seconds']); + } + //this checks if the user's values have changed, in which case we need to update the user details throughout + //the back office similar to how we do when a user logs in + if (headers['x-umb-user-modified']) { + eventsService.emit('app.userRefresh'); } - return promise; - }, function(originalResponse) { + }, function (originalResponse) { // Intercept failed requests - // Make sure we have the configuration of the request (don't we always?) var config = originalResponse.config ? originalResponse.config : {}; - // Make sure we have an object for the headers of the request var headers = config.headers ? config.headers : {}; - //Here we'll check if we should ignore the error (either based on the original header set or the request configuration) - if (headers["x-umb-ignore-error"] === "ignore" || config.umbIgnoreErrors === true || (angular.isArray(config.umbIgnoreStatus) && config.umbIgnoreStatus.indexOf(originalResponse.status) !== -1)) { + if (headers['x-umb-ignore-error'] === 'ignore' || config.umbIgnoreErrors === true || angular.isArray(config.umbIgnoreStatus) && config.umbIgnoreStatus.indexOf(originalResponse.status) !== -1) { //exit/ignore return promise; } - var filtered = _.find(requestInterceptorFilter(), function(val) { + var filtered = _.find(requestInterceptorFilter(), function (val) { return config.url.indexOf(val) > 0; }); if (filtered) { return promise; } - //A 401 means that the user is not logged in - if (originalResponse.status === 401) { - - var userService = $injector.get('userService'); // see above - - //Associate the user name with the retry to ensure we retry for the right user - promise = userService.getCurrentUser() - .then(function (user) { - var userName = user ? user.name : null; - //The request bounced because it was not authorized - add a new request to the retry queue - return queue.pushRetryFn('unauthorized-server', userName, function retryRequest() { - // We must use $injector to get the $http service to prevent circular dependency - return $injector.get('$http')(originalResponse.config); - }); + if (originalResponse.status === 401 && !originalResponse.config.url.endsWith('umbraco/backoffice/UmbracoApi/Authentication/GetCurrentUser')) { + var userService = $injector.get('userService'); + // see above + //Associate the user name with the retry to ensure we retry for the right user + promise = userService.getCurrentUser().then(function (user) { + var userName = user ? user.name : null; + //The request bounced because it was not authorized - add a new request to the retry queue + return queue.pushRetryFn('unauthorized-server', userName, function retryRequest() { + // We must use $injector to get the $http service to prevent circular dependency + return $injector.get('$http')(originalResponse.config); + }); }); - } - else if (originalResponse.status === 404) { - + } else if (originalResponse.status === 404) { //a 404 indicates that the request was not found - this could be due to a non existing url, or it could //be due to accessing a url with a parameter that doesn't exist, either way we should notifiy the user about it - - var errMsg = "The URL returned a 404 (not found):
    " + originalResponse.config.url.split('?')[0] + ""; + var errMsg = 'The URL returned a 404 (not found):
    ' + originalResponse.config.url.split('?')[0] + ''; if (originalResponse.data && originalResponse.data.ExceptionMessage) { - errMsg += "
    with error:
    " + originalResponse.data.ExceptionMessage + ""; + errMsg += '
    with error:
    ' + originalResponse.data.ExceptionMessage + ''; } if (originalResponse.config.data) { - errMsg += "
    with data:
    " + angular.toJson(originalResponse.config.data) + "
    Contact your administrator for information."; + errMsg += '
    with data:
    ' + angular.toJson(originalResponse.config.data) + '
    Contact your administrator for information.'; } - - notifications.error( - "Request error", - errMsg); - - } - else if (originalResponse.status === 403) { + notifications.error('Request error', errMsg); + } else if (originalResponse.status === 403) { //if the status was a 403 it means the user didn't have permission to do what the request was trying to do. //How do we deal with this now, need to tell the user somehow that they don't have permission to do the thing that was //requested. We can either deal with this globally here, or we can deal with it globally for individual requests on the umbRequestHelper, // or completely custom for services calling resources. - //http://issues.umbraco.org/issue/U4-2749 - //It was decided to just put these messages into the normal status messages. - - var msg = "Unauthorized access to URL:
    " + originalResponse.config.url.split('?')[0] + ""; + var msg = 'Unauthorized access to URL:
    ' + originalResponse.config.url.split('?')[0] + ''; if (originalResponse.config.data) { - msg += "
    with data:
    " + angular.toJson(originalResponse.config.data) + "
    Contact your administrator for information."; + msg += '
    with data:
    ' + angular.toJson(originalResponse.config.data) + '
    Contact your administrator for information.'; } - - notifications.error( - "Authorization error", - msg); + notifications.error('Authorization error', msg); } - return promise; }); + }; + } + ]) //used to set headers on all requests where necessary +.factory('umbracoRequestInterceptor', function ($q, queryStrings) { + return { + //dealing with requests: + 'request': function (config) { + if (queryStrings.getParams().umbDebug === 'true' || queryStrings.getParams().umbdebug === 'true') { + config.headers['X-UMB-DEBUG'] = 'true'; + } + return config; + } }; - }]) - - .value('requestInterceptorFilter', function() { - return ["www.gravatar.com"]; - }) - - // We have to add the interceptor to the queue as a string because the interceptor depends upon service instances that are not available in the config block. - .config(['$httpProvider', function ($httpProvider) { - $httpProvider.responseInterceptors.push('securityInterceptor'); - }]); - - -})(); \ No newline at end of file + }).value('requestInterceptorFilter', function () { + return ['www.gravatar.com']; + }) // We have to add the interceptor to the queue as a string because the interceptor depends upon service instances that are not available in the config block. +.config([ + '$httpProvider', + function ($httpProvider) { + $httpProvider.defaults.xsrfHeaderName = 'X-UMB-XSRF-TOKEN'; + $httpProvider.defaults.xsrfCookieName = 'UMB-XSRF-TOKEN'; + $httpProvider.responseInterceptors.push('securityInterceptor'); + $httpProvider.interceptors.push('umbracoRequestInterceptor'); + } + ]); +}()); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.services.js b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.services.js index cca58edb..b00a2836 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.services.js +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/umbraco.services.js @@ -1,25 +1,19 @@ -/*! umbraco - * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2017 Umbraco HQ; - * Licensed - */ - -(function() { - -angular.module("umbraco.services", ["umbraco.security", "umbraco.resources"]); - -/** +(function () { + angular.module('umbraco.services', [ + 'umbraco.security', + 'umbraco.resources' + ]); + /** * @ngdoc service * @name umbraco.services.angularHelper * @function * * @description * Some angular helper/extension methods - */ -function angularHelper($log, $q) { - return { - - /** + */ + function angularHelper($log, $q) { + return { + /** * @ngdoc function * @name umbraco.services.angularHelper#rejectedPromise * @methodOf umbraco.services.angularHelper @@ -30,15 +24,14 @@ function angularHelper($log, $q) { * is a wrapper to do that so we can save on writing a bit of code. * * @param {object} objReject The object to send back with the promise rejection - */ - rejectedPromise: function (objReject) { - var deferred = $q.defer(); - //return an error object including the error message for UI - deferred.reject(objReject); - return deferred.promise; - }, - - /** + */ + rejectedPromise: function (objReject) { + var deferred = $q.defer(); + //return an error object including the error message for UI + deferred.reject(objReject); + return deferred.promise; + }, + /** * @ngdoc function * @name safeApply * @methodOf umbraco.services.angularHelper @@ -46,24 +39,21 @@ function angularHelper($log, $q) { * * @description * This checks if a digest/apply is already occuring, if not it will force an apply call - */ - safeApply: function (scope, fn) { - if (scope.$$phase || scope.$root.$$phase) { - if (angular.isFunction(fn)) { - fn(); - } - } - else { - if (angular.isFunction(fn)) { - scope.$apply(fn); - } - else { - scope.$apply(); - } - } - }, - - /** + */ + safeApply: function (scope, fn) { + if (scope.$$phase || scope.$root.$$phase) { + if (angular.isFunction(fn)) { + fn(); + } + } else { + if (angular.isFunction(fn)) { + scope.$apply(fn); + } else { + scope.$apply(); + } + } + }, + /** * @ngdoc function * @name getCurrentForm * @methodOf umbraco.services.angularHelper @@ -71,55 +61,51 @@ function angularHelper($log, $q) { * * @description * Returns the current form object applied to the scope or null if one is not found - */ - getCurrentForm: function (scope) { - - //NOTE: There isn't a way in angular to get a reference to the current form object since the form object - // is just defined as a property of the scope when it is named but you'll always need to know the name which - // isn't very convenient. If we want to watch for validation changes we need to get a form reference. - // The way that we detect the form object is a bit hackerific in that we detect all of the required properties - // that exist on a form object. - // - //The other way to do it in a directive is to require "^form", but in a controller the only other way to do it - // is to inject the $element object and use: $element.inheritedData('$formController'); - - var form = null; - //var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$invalid", "$addControl", "$removeControl", "$setValidity", "$setDirty"]; - var requiredFormProps = ["$addControl", "$removeControl", "$setValidity", "$setDirty", "$setPristine"]; - - // a method to check that the collection of object prop names contains the property name expected - function propertyExists(objectPropNames) { - //ensure that every required property name exists on the current scope property - return _.every(requiredFormProps, function (item) { - - return _.contains(objectPropNames, item); - }); - } - - for (var p in scope) { - - if (_.isObject(scope[p]) && p !== "this" && p.substr(0, 1) !== "$") { - //get the keys of the property names for the current property - var props = _.keys(scope[p]); - //if the length isn't correct, try the next prop - if (props.length < requiredFormProps.length) { - continue; - } - - //ensure that every required property name exists on the current scope property - var containProperty = propertyExists(props); - - if (containProperty) { - form = scope[p]; - break; - } - } - } - - return form; - }, - - /** + */ + getCurrentForm: function (scope) { + //NOTE: There isn't a way in angular to get a reference to the current form object since the form object + // is just defined as a property of the scope when it is named but you'll always need to know the name which + // isn't very convenient. If we want to watch for validation changes we need to get a form reference. + // The way that we detect the form object is a bit hackerific in that we detect all of the required properties + // that exist on a form object. + // + //The other way to do it in a directive is to require "^form", but in a controller the only other way to do it + // is to inject the $element object and use: $element.inheritedData('$formController'); + var form = null; + //var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$invalid", "$addControl", "$removeControl", "$setValidity", "$setDirty"]; + var requiredFormProps = [ + '$addControl', + '$removeControl', + '$setValidity', + '$setDirty', + '$setPristine' + ]; + // a method to check that the collection of object prop names contains the property name expected + function propertyExists(objectPropNames) { + //ensure that every required property name exists on the current scope property + return _.every(requiredFormProps, function (item) { + return _.contains(objectPropNames, item); + }); + } + for (var p in scope) { + if (_.isObject(scope[p]) && p !== 'this' && p.substr(0, 1) !== '$') { + //get the keys of the property names for the current property + var props = _.keys(scope[p]); + //if the length isn't correct, try the next prop + if (props.length < requiredFormProps.length) { + continue; + } + //ensure that every required property name exists on the current scope property + var containProperty = propertyExists(props); + if (containProperty) { + form = scope[p]; + break; + } + } + } + return form; + }, + /** * @ngdoc function * @name validateHasForm * @methodOf umbraco.services.angularHelper @@ -128,16 +114,15 @@ function angularHelper($log, $q) { * @description * This will validate that the current scope has an assigned form object, if it doesn't an exception is thrown, if * it does we return the form object. - */ - getRequiredCurrentForm: function (scope) { - var currentForm = this.getCurrentForm(scope); - if (!currentForm || !currentForm.$name) { - throw "The current scope requires a current form object (or ng-form) with a name assigned to it"; - } - return currentForm; - }, - - /** + */ + getRequiredCurrentForm: function (scope) { + var currentForm = this.getCurrentForm(scope); + if (!currentForm || !currentForm.$name) { + throw 'The current scope requires a current form object (or ng-form) with a name assigned to it'; + } + return currentForm; + }, + /** * @ngdoc function * @name getNullForm * @methodOf umbraco.services.angularHelper @@ -149,329 +134,306 @@ function angularHelper($log, $q) { * any of this publicly to us, so we need to create our own. * * @param {string} formName The form name to assign - */ - getNullForm: function (formName) { - return { - $addControl: angular.noop, - $removeControl: angular.noop, - $setValidity: angular.noop, - $setDirty: angular.noop, - $setPristine: angular.noop, - $name: formName - //NOTE: we don't include the 'properties', just the methods. - }; - } - }; -} -angular.module('umbraco.services').factory('angularHelper', angularHelper); -/** - * @ngdoc service - * @name umbraco.services.appState - * @function - * - * @description - * Tracks the various application state variables when working in the back office, raises events when state changes. - * - * ##Samples - * - * ####Subscribe to global state changes: - * - *
    -  *    scope.showTree = appState.getGlobalState("showNavigation");
    -  *
    -  *    eventsService.on("appState.globalState.changed", function (e, args) {
    -  *               if (args.key === "showNavigation") {
    -  *                   scope.showTree = args.value;
    -  *               }
    -  *           });  
    -  * 
    - * - * ####Subscribe to section-state changes - * - *
    - *    scope.currentSection = appState.getSectionState("currentSection");
    - *
    - *    eventsService.on("appState.sectionState.changed", function (e, args) {
    - *               if (args.key === "currentSection") {
    - *                   scope.currentSection = args.value;
    - *               }
    - *           });  
    - * 
    + */ + getNullForm: function (formName) { + return { + $addControl: angular.noop, + $removeControl: angular.noop, + $setValidity: angular.noop, + $setDirty: angular.noop, + $setPristine: angular.noop, + $name: formName //NOTE: we don't include the 'properties', just the methods. + }; + } + }; + } + angular.module('umbraco.services').factory('angularHelper', angularHelper); + /** + * @ngdoc service + * @name umbraco.services.appState + * @function + * + * @description + * Tracks the various application state variables when working in the back office, raises events when state changes. + * + * ##Samples + * + * ####Subscribe to global state changes: + * + *
    +  *    scope.showTree = appState.getGlobalState("showNavigation");
    +  *
    +  *    eventsService.on("appState.globalState.changed", function (e, args) {
    +  *               if (args.key === "showNavigation") {
    +  *                   scope.showTree = args.value;
    +  *               }
    +  *           });  
    +  * 
    + * + * ####Subscribe to section-state changes + * + *
    + *    scope.currentSection = appState.getSectionState("currentSection");
    + *
    + *    eventsService.on("appState.sectionState.changed", function (e, args) {
    + *               if (args.key === "currentSection") {
    + *                   scope.currentSection = args.value;
    + *               }
    + *           });  
    + * 
    */ -function appState(eventsService) { - - //Define all variables here - we are never returning this objects so they cannot be publicly mutable - // changed, we only expose methods to interact with the values. - - var globalState = { - showNavigation: null, - touchDevice: null, - showTray: null, - stickyNavigation: null, - navMode: null, - isReady: null, - isTablet: null - }; - - var sectionState = { - //The currently active section - currentSection: null, - showSearchResults: null - }; - - var treeState = { - //The currently selected node - selectedNode: null, - //The currently loaded root node reference - depending on the section loaded this could be a section root or a normal root. - //We keep this reference so we can lookup nodes to interact with in the UI via the tree service - currentRootNode: null - }; - - var menuState = { - //this list of menu items to display - menuActions: null, - //the title to display in the context menu dialog - dialogTitle: null, - //The tree node that the ctx menu is launched for - currentNode: null, - //Whether the menu's dialog is being shown or not - showMenuDialog: null, - //Whether the context menu is being shown or not - showMenu: null - }; - - /** function to validate and set the state on a state object */ - function setState(stateObj, key, value, stateObjName) { - if (!_.has(stateObj, key)) { - throw "The variable " + key + " does not exist in " + stateObjName; - } - var changed = stateObj[key] !== value; - stateObj[key] = value; - if (changed) { - eventsService.emit("appState." + stateObjName + ".changed", { key: key, value: value }); + function appState(eventsService) { + //Define all variables here - we are never returning this objects so they cannot be publicly mutable + // changed, we only expose methods to interact with the values. + var globalState = { + showNavigation: null, + touchDevice: null, + showTray: null, + stickyNavigation: null, + navMode: null, + isReady: null, + isTablet: null + }; + var sectionState = { + //The currently active section + currentSection: null, + showSearchResults: null + }; + var treeState = { + //The currently selected node + selectedNode: null, + //The currently loaded root node reference - depending on the section loaded this could be a section root or a normal root. + //We keep this reference so we can lookup nodes to interact with in the UI via the tree service + currentRootNode: null + }; + var menuState = { + //this list of menu items to display + menuActions: null, + //the title to display in the context menu dialog + dialogTitle: null, + //The tree node that the ctx menu is launched for + currentNode: null, + //Whether the menu's dialog is being shown or not + showMenuDialog: null, + //Whether the context menu is being shown or not + showMenu: null + }; + /** function to validate and set the state on a state object */ + function setState(stateObj, key, value, stateObjName) { + if (!_.has(stateObj, key)) { + throw 'The variable ' + key + ' does not exist in ' + stateObjName; + } + var changed = stateObj[key] !== value; + stateObj[key] = value; + if (changed) { + eventsService.emit('appState.' + stateObjName + '.changed', { + key: key, + value: value + }); + } } - } - - /** function to validate and set the state on a state object */ - function getState(stateObj, key, stateObjName) { - if (!_.has(stateObj, key)) { - throw "The variable " + key + " does not exist in " + stateObjName; + /** function to validate and set the state on a state object */ + function getState(stateObj, key, stateObjName) { + if (!_.has(stateObj, key)) { + throw 'The variable ' + key + ' does not exist in ' + stateObjName; + } + return stateObj[key]; } - return stateObj[key]; - } - - return { - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getGlobalState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Returns the current global state value by key - we do not return an object reference here - we do NOT want this - * to be publicly mutable and allow setting arbitrary values - * + return { + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getGlobalState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Returns the current global state value by key - we do not return an object reference here - we do NOT want this + * to be publicly mutable and allow setting arbitrary values + * */ - getGlobalState: function (key) { - return getState(globalState, key, "globalState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#setGlobalState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Sets a global state value by key - * + getGlobalState: function (key) { + return getState(globalState, key, 'globalState'); + }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#setGlobalState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Sets a global state value by key + * */ - setGlobalState: function (key, value) { - setState(globalState, key, value, "globalState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getSectionState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Returns the current section state value by key - we do not return an object here - we do NOT want this - * to be publicly mutable and allow setting arbitrary values - * + setGlobalState: function (key, value) { + setState(globalState, key, value, 'globalState'); + }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getSectionState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Returns the current section state value by key - we do not return an object here - we do NOT want this + * to be publicly mutable and allow setting arbitrary values + * */ - getSectionState: function (key) { - return getState(sectionState, key, "sectionState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#setSectionState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Sets a section state value by key - * + getSectionState: function (key) { + return getState(sectionState, key, 'sectionState'); + }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#setSectionState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Sets a section state value by key + * */ - setSectionState: function(key, value) { - setState(sectionState, key, value, "sectionState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getTreeState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Returns the current tree state value by key - we do not return an object here - we do NOT want this - * to be publicly mutable and allow setting arbitrary values - * + setSectionState: function (key, value) { + setState(sectionState, key, value, 'sectionState'); + }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getTreeState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Returns the current tree state value by key - we do not return an object here - we do NOT want this + * to be publicly mutable and allow setting arbitrary values + * */ - getTreeState: function (key) { - return getState(treeState, key, "treeState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#setTreeState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Sets a section state value by key - * + getTreeState: function (key) { + return getState(treeState, key, 'treeState'); + }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#setTreeState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Sets a section state value by key + * */ - setTreeState: function (key, value) { - setState(treeState, key, value, "treeState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getMenuState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Returns the current menu state value by key - we do not return an object here - we do NOT want this - * to be publicly mutable and allow setting arbitrary values - * + setTreeState: function (key, value) { + setState(treeState, key, value, 'treeState'); + }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getMenuState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Returns the current menu state value by key - we do not return an object here - we do NOT want this + * to be publicly mutable and allow setting arbitrary values + * */ - getMenuState: function (key) { - return getState(menuState, key, "menuState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#setMenuState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Sets a section state value by key - * + getMenuState: function (key) { + return getState(menuState, key, 'menuState'); + }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#setMenuState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Sets a section state value by key + * */ - setMenuState: function (key, value) { - setState(menuState, key, value, "menuState"); - }, - - }; -} -angular.module('umbraco.services').factory('appState', appState); - -/** - * @ngdoc service - * @name umbraco.services.editorState - * @function - * - * @description - * Tracks the parent object for complex editors by exposing it as - * an object reference via editorState.current.entity - * - * it is possible to modify this object, so should be used with care + setMenuState: function (key, value) { + setState(menuState, key, value, 'menuState'); + } + }; + } + angular.module('umbraco.services').factory('appState', appState); + /** + * @ngdoc service + * @name umbraco.services.editorState + * @function + * + * @description + * Tracks the parent object for complex editors by exposing it as + * an object reference via editorState.current.entity + * + * it is possible to modify this object, so should be used with care */ -angular.module('umbraco.services').factory("editorState", function() { - - var current = null; - var state = { - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#set - * @methodOf umbraco.services.editorState - * @function - * - * @description - * Sets the current entity object for the currently active editor - * This is only used when implementing an editor with a complex model - * like the content editor, where the model is modified by several - * child controllers. + angular.module('umbraco.services').factory('editorState', function () { + var current = null; + var state = { + /** + * @ngdoc function + * @name umbraco.services.angularHelper#set + * @methodOf umbraco.services.editorState + * @function + * + * @description + * Sets the current entity object for the currently active editor + * This is only used when implementing an editor with a complex model + * like the content editor, where the model is modified by several + * child controllers. */ - set: function (entity) { - current = entity; - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#reset - * @methodOf umbraco.services.editorState - * @function - * - * @description - * Since the editorstate entity is read-only, you cannot set it to null - * only through the reset() method + set: function (entity) { + current = entity; + }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#reset + * @methodOf umbraco.services.editorState + * @function + * + * @description + * Since the editorstate entity is read-only, you cannot set it to null + * only through the reset() method */ - reset: function() { - current = null; - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getCurrent - * @methodOf umbraco.services.editorState - * @function - * - * @description - * Returns an object reference to the current editor entity. - * the entity is the root object of the editor. - * EditorState is used by property/parameter editors that need - * access to the entire entity being edited, not just the property/parameter - * - * editorState.current can not be overwritten, you should only read values from it - * since modifying individual properties should be handled by the property editors + reset: function () { + current = null; + }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getCurrent + * @methodOf umbraco.services.editorState + * @function + * + * @description + * Returns an object reference to the current editor entity. + * the entity is the root object of the editor. + * EditorState is used by property/parameter editors that need + * access to the entire entity being edited, not just the property/parameter + * + * editorState.current can not be overwritten, you should only read values from it + * since modifying individual properties should be handled by the property editors */ - getCurrent: function() { - return current; - } - }; - - //TODO: This shouldn't be removed! use getCurrent() method instead of a hacked readonly property which is confusing. - - //create a get/set property but don't allow setting - Object.defineProperty(state, "current", { - get: function () { - return current; - }, - set: function (value) { - throw "Use editorState.set to set the value of the current entity"; - }, + getCurrent: function () { + return current; + } + }; + //TODO: This shouldn't be removed! use getCurrent() method instead of a hacked readonly property which is confusing. + //create a get/set property but don't allow setting + Object.defineProperty(state, 'current', { + get: function () { + return current; + }, + set: function (value) { + throw 'Use editorState.set to set the value of the current entity'; + } + }); + return state; }); - - return state; -}); -/** + /** * @ngdoc service * @name umbraco.services.assetsService * - * @requires $q + * @requires $q * @requires angularHelper - * + * * @description * Promise-based utillity service to lazy-load client-side dependencies inside angular controllers. - * + * * ##usage * To use, simply inject the assetsService into any controller that needs it, and make * sure the umbraco.services module is accesible - which it should be by default. @@ -482,7 +444,7 @@ angular.module('umbraco.services').factory("editorState", function() { * //this code executes when the dependencies are done loading * }); * }); - * + * * * You can also load individual files, which gives you greater control over what attibutes are passed to the file, as well as timeout * @@ -502,452 +464,433 @@ angular.module('umbraco.services').factory("editorState", function() { * //loadcss cannot determine when the css is done loading, so this will trigger instantly * }); * }); - * - */ -angular.module('umbraco.services') -.factory('assetsService', function ($q, $log, angularHelper, umbRequestHelper, $rootScope, $http) { - - var initAssetsLoaded = false; - var appendRnd = function (url) { - //if we don't have a global umbraco obj yet, the app is bootstrapping - if (!Umbraco.Sys.ServerVariables.application) { - return url; - } - - var rnd = Umbraco.Sys.ServerVariables.application.version + "." + Umbraco.Sys.ServerVariables.application.cdf; - var _op = (url.indexOf("?") > 0) ? "&" : "?"; - url = url + _op + "umb__rnd=" + rnd; - return url; - }; - - function convertVirtualPath(path) { - //make this work for virtual paths - if (path.startsWith("~/")) { - path = umbRequestHelper.convertVirtualToAbsolutePath(path); - } - return path; - } - - var service = { - loadedAssets: {}, - - _getAssetPromise: function (path) { - - if (this.loadedAssets[path]) { - return this.loadedAssets[path]; - } else { - var deferred = $q.defer(); - this.loadedAssets[path] = { deferred: deferred, state: "new", path: path }; - return this.loadedAssets[path]; - } - }, - /** + * + */ + angular.module('umbraco.services').factory('assetsService', function ($q, $log, angularHelper, umbRequestHelper, $rootScope, $http) { + var initAssetsLoaded = false; + function appendRnd(url) { + //if we don't have a global umbraco obj yet, the app is bootstrapping + if (!Umbraco.Sys.ServerVariables.application) { + return url; + } + var rnd = Umbraco.Sys.ServerVariables.application.cacheBuster; + var _op = url.indexOf('?') > 0 ? '&' : '?'; + url = url + _op + 'umb__rnd=' + rnd; + return url; + } + ; + function convertVirtualPath(path) { + //make this work for virtual paths + if (path.startsWith('~/')) { + path = umbRequestHelper.convertVirtualToAbsolutePath(path); + } + return path; + } + var service = { + loadedAssets: {}, + _getAssetPromise: function (path) { + if (this.loadedAssets[path]) { + return this.loadedAssets[path]; + } else { + var deferred = $q.defer(); + this.loadedAssets[path] = { + deferred: deferred, + state: 'new', + path: path + }; + return this.loadedAssets[path]; + } + }, + /** Internal method. This is called when the application is loading and the user is already authenticated, or once the user is authenticated. There's a few assets the need to be loaded for the application to function but these assets require authentication to load. - */ - _loadInitAssets: function () { - var deferred = $q.defer(); - //here we need to ensure the required application assets are loaded - if (initAssetsLoaded === false) { - var self = this; - self.loadJs(umbRequestHelper.getApiUrl("serverVarsJs", "", ""), $rootScope).then(function () { - initAssetsLoaded = true; - - //now we need to go get the legacyTreeJs - but this can be done async without waiting. - self.loadJs(umbRequestHelper.getApiUrl("legacyTreeJs", "", ""), $rootScope); - - deferred.resolve(); - }); - } - else { - deferred.resolve(); - } - return deferred.promise; - }, - - /** + */ + _loadInitAssets: function () { + var deferred = $q.defer(); + //here we need to ensure the required application assets are loaded + if (initAssetsLoaded === false) { + var self = this; + self.loadJs(umbRequestHelper.getApiUrl('serverVarsJs', '', ''), $rootScope).then(function () { + initAssetsLoaded = true; + //now we need to go get the legacyTreeJs - but this can be done async without waiting. + self.loadJs(umbRequestHelper.getApiUrl('legacyTreeJs', '', ''), $rootScope); + deferred.resolve(); + }); + } else { + deferred.resolve(); + } + return deferred.promise; + }, + /** * @ngdoc method * @name umbraco.services.assetsService#loadCss * @methodOf umbraco.services.assetsService * * @description * Injects a file as a stylesheet into the document head - * + * * @param {String} path path to the css file to load * @param {Scope} scope optional scope to pass into the loader - * @param {Object} keyvalue collection of attributes to pass to the stylesheet element + * @param {Object} keyvalue collection of attributes to pass to the stylesheet element * @param {Number} timeout in milliseconds * @returns {Promise} Promise object which resolves when the file has loaded - */ - loadCss: function (path, scope, attributes, timeout) { - - path = convertVirtualPath(path); - - var asset = this._getAssetPromise(path); // $q.defer(); - var t = timeout || 5000; - var a = attributes || undefined; - - if (asset.state === "new") { - asset.state = "loading"; - LazyLoad.css(appendRnd(path), function () { - if (!scope) { - asset.state = "loaded"; - asset.deferred.resolve(true); - } else { - asset.state = "loaded"; - angularHelper.safeApply(scope, function () { - asset.deferred.resolve(true); - }); - } - }); - } else if (asset.state === "loaded") { - asset.deferred.resolve(true); - } - return asset.deferred.promise; - }, - - /** + */ + loadCss: function (path, scope, attributes, timeout) { + path = convertVirtualPath(path); + var asset = this._getAssetPromise(path); + // $q.defer(); + var t = timeout || 5000; + var a = attributes || undefined; + if (asset.state === 'new') { + asset.state = 'loading'; + LazyLoad.css(appendRnd(path), function () { + if (!scope) { + asset.state = 'loaded'; + asset.deferred.resolve(true); + } else { + asset.state = 'loaded'; + angularHelper.safeApply(scope, function () { + asset.deferred.resolve(true); + }); + } + }); + } else if (asset.state === 'loaded') { + asset.deferred.resolve(true); + } + return asset.deferred.promise; + }, + /** * @ngdoc method * @name umbraco.services.assetsService#loadJs * @methodOf umbraco.services.assetsService * * @description * Injects a file as a javascript into the document - * + * * @param {String} path path to the js file to load * @param {Scope} scope optional scope to pass into the loader - * @param {Object} keyvalue collection of attributes to pass to the script element + * @param {Object} keyvalue collection of attributes to pass to the script element * @param {Number} timeout in milliseconds * @returns {Promise} Promise object which resolves when the file has loaded - */ - loadJs: function (path, scope, attributes, timeout) { - - path = convertVirtualPath(path); - - var asset = this._getAssetPromise(path); // $q.defer(); - var t = timeout || 5000; - var a = attributes || undefined; - - if (asset.state === "new") { - asset.state = "loading"; - - LazyLoad.js(appendRnd(path), function () { - if (!scope) { - asset.state = "loaded"; - asset.deferred.resolve(true); - } else { - asset.state = "loaded"; - angularHelper.safeApply(scope, function () { - asset.deferred.resolve(true); - }); - } - }); - - } else if (asset.state === "loaded") { - asset.deferred.resolve(true); - } - - return asset.deferred.promise; - }, - - /** + */ + loadJs: function (path, scope, attributes, timeout) { + path = convertVirtualPath(path); + var asset = this._getAssetPromise(path); + // $q.defer(); + var t = timeout || 5000; + var a = attributes || undefined; + if (asset.state === 'new') { + asset.state = 'loading'; + LazyLoad.js(appendRnd(path), function () { + if (!scope) { + asset.state = 'loaded'; + asset.deferred.resolve(true); + } else { + asset.state = 'loaded'; + angularHelper.safeApply(scope, function () { + asset.deferred.resolve(true); + }); + } + }); + } else if (asset.state === 'loaded') { + asset.deferred.resolve(true); + } + return asset.deferred.promise; + }, + /** * @ngdoc method * @name umbraco.services.assetsService#load * @methodOf umbraco.services.assetsService * * @description - * Injects a collection of files, this can be ONLY js files - * + * Injects a collection of css and js files + * * * @param {Array} pathArray string array of paths to the files to load * @param {Scope} scope optional scope to pass into the loader * @returns {Promise} Promise object which resolves when all the files has loaded - */ - load: function (pathArray, scope) { - var promise; - - if (!angular.isArray(pathArray)) { - throw "pathArray must be an array"; - } - - var nonEmpty = _.reject(pathArray, function (item) { - return item === undefined || item === ""; - }); - - - //don't load anything if there's nothing to load - if (nonEmpty.length > 0) { - var promises = []; - var assets = []; - - //compile a list of promises - //blocking - _.each(nonEmpty, function (path) { - - path = convertVirtualPath(path); - - var asset = service._getAssetPromise(path); - //if not previously loaded, add to list of promises - if (asset.state !== "loaded") { - if (asset.state === "new") { - asset.state = "loading"; - assets.push(asset); - } - - //we need to always push to the promises collection to monitor correct - //execution - promises.push(asset.deferred.promise); - } - }); - - - //gives a central monitoring of all assets to load - promise = $q.all(promises); - - _.each(assets, function (asset) { - LazyLoad.js(appendRnd(asset.path), function () { - asset.state = "loaded"; - if (!scope) { - asset.deferred.resolve(true); - } - else { - angularHelper.safeApply(scope, function () { - asset.deferred.resolve(true); - }); - } - }); - }); - } - else { - //return and resolve - var deferred = $q.defer(); - promise = deferred.promise; - deferred.resolve(true); - } - - - return promise; - } - }; - - return service; -}); - -/** + */ + load: function (pathArray, scope) { + var promise; + if (!angular.isArray(pathArray)) { + throw 'pathArray must be an array'; + } + // Check to see if there's anything to load, resolve promise if not + var nonEmpty = _.reject(pathArray, function (item) { + return item === undefined || item === ''; + }); + if (nonEmpty.length === 0) { + var deferred = $q.defer(); + promise = deferred.promise; + deferred.resolve(true); + return promise; + } + //compile a list of promises + //blocking + var promises = []; + var assets = []; + _.each(nonEmpty, function (path) { + path = convertVirtualPath(path); + var asset = service._getAssetPromise(path); + //if not previously loaded, add to list of promises + if (asset.state !== 'loaded') { + if (asset.state === 'new') { + asset.state = 'loading'; + assets.push(asset); + } + //we need to always push to the promises collection to monitor correct + //execution + promises.push(asset.deferred.promise); + } + }); + //gives a central monitoring of all assets to load + promise = $q.all(promises); + // Split into css and js asset arrays, and use LazyLoad on each array + var cssAssets = _.filter(assets, function (asset) { + return asset.path.match(/(\.css$|\.css\?)/ig); + }); + var jsAssets = _.filter(assets, function (asset) { + return asset.path.match(/(\.js$|\.js\?)/ig); + }); + function assetLoaded(asset) { + asset.state = 'loaded'; + if (!scope) { + asset.deferred.resolve(true); + return; + } + angularHelper.safeApply(scope, function () { + asset.deferred.resolve(true); + }); + } + if (cssAssets.length > 0) { + var cssPaths = _.map(cssAssets, function (asset) { + return appendRnd(asset.path); + }); + LazyLoad.css(cssPaths, function () { + _.each(cssAssets, assetLoaded); + }); + } + if (jsAssets.length > 0) { + var jsPaths = _.map(jsAssets, function (asset) { + return appendRnd(asset.path); + }); + LazyLoad.js(jsPaths, function () { + _.each(jsAssets, assetLoaded); + }); + } + return promise; + } + }; + return service; + }); + /** * @ngdoc service * @name umbraco.services.contentEditingHelper * @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by * all editors to share logic and reduce the amount of replicated code among editors. -**/ -function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState) { - - function isValidIdentifier(id){ - //empty id <= 0 - if(angular.isNumber(id) && id > 0){ - return true; - } - - //empty guid - if(id === "00000000-0000-0000-0000-000000000000"){ - return false; - } - - //empty string / alias - if(id === ""){ - return false; - } - - return true; - } - - return { - - /** Used by the content editor and mini content editor to perform saving operations */ - contentEditorPerformSave: function (args) { - if (!angular.isObject(args)) { - throw "args must be an object"; - } - if (!args.scope) { - throw "args.scope is not defined"; - } - if (!args.content) { - throw "args.content is not defined"; - } - if (!args.statusMessage) { - throw "args.statusMessage is not defined"; - } - if (!args.saveMethod) { - throw "args.saveMethod is not defined"; - } - - var redirectOnFailure = args.redirectOnFailure !== undefined ? args.redirectOnFailure : true; - - var self = this; - - //we will use the default one for content if not specified - var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; - - var deferred = $q.defer(); - - if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage, action: args.action })) { - - args.scope.busy = true; - - args.saveMethod(args.content, $routeParams.create, fileManager.getFiles()) - .then(function (data) { - - formHelper.resetForm({ scope: args.scope, notifications: data.notifications }); - - self.handleSuccessfulSave({ - scope: args.scope, - savedContent: data, - rebindCallback: function() { - rebindCallback.apply(self, [args.content, data]); - } - }); - - args.scope.busy = false; - deferred.resolve(data); - - }, function (err) { - self.handleSaveError({ - redirectOnFailure: redirectOnFailure, - err: err, - rebindCallback: function() { - rebindCallback.apply(self, [args.content, err.data]); - } - }); - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - args.scope.busy = false; - deferred.reject(err); - }); - } - else { - deferred.reject(); - } - - return deferred.promise; - }, - - - /** Returns the action button definitions based on what permissions the user has. +**/ + function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState) { + function isValidIdentifier(id) { + //empty id <= 0 + if (angular.isNumber(id) && id > 0) { + return true; + } + //empty guid + if (id === '00000000-0000-0000-0000-000000000000') { + return false; + } + //empty string / alias + if (id === '') { + return false; + } + return true; + } + return { + /** Used by the content editor and mini content editor to perform saving operations */ + //TODO: Make this a more helpful/reusable method for other form operations! we can simplify this form most forms + contentEditorPerformSave: function (args) { + if (!angular.isObject(args)) { + throw 'args must be an object'; + } + if (!args.scope) { + throw 'args.scope is not defined'; + } + if (!args.content) { + throw 'args.content is not defined'; + } + if (!args.statusMessage) { + throw 'args.statusMessage is not defined'; + } + if (!args.saveMethod) { + throw 'args.saveMethod is not defined'; + } + var redirectOnFailure = args.redirectOnFailure !== undefined ? args.redirectOnFailure : true; + var self = this; + //we will use the default one for content if not specified + var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; + var deferred = $q.defer(); + if (!args.scope.busy && formHelper.submitForm({ + scope: args.scope, + statusMessage: args.statusMessage, + action: args.action + })) { + args.scope.busy = true; + args.saveMethod(args.content, $routeParams.create, fileManager.getFiles()).then(function (data) { + formHelper.resetForm({ + scope: args.scope, + notifications: data.notifications + }); + self.handleSuccessfulSave({ + scope: args.scope, + savedContent: data, + rebindCallback: function () { + rebindCallback.apply(self, [ + args.content, + data + ]); + } + }); + args.scope.busy = false; + deferred.resolve(data); + }, function (err) { + self.handleSaveError({ + redirectOnFailure: redirectOnFailure, + err: err, + rebindCallback: function () { + rebindCallback.apply(self, [ + args.content, + err.data + ]); + } + }); + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + args.scope.busy = false; + deferred.reject(err); + }); + } else { + deferred.reject(); + } + return deferred.promise; + }, + /** Returns the action button definitions based on what permissions the user has. The content.allowedActions parameter contains a list of chars, each represents a button by permission so - here we'll build the buttons according to the chars of the user. */ - configureContentEditorButtons: function (args) { - - if (!angular.isObject(args)) { - throw "args must be an object"; - } - if (!args.content) { - throw "args.content is not defined"; - } - if (!args.methods) { - throw "args.methods is not defined"; - } - if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.save || !args.methods.unPublish) { - throw "args.methods does not contain all required defined methods"; - } - - var buttons = { - defaultButton: null, - subButtons: [] - }; - - function createButtonDefinition(ch) { - switch (ch) { - case "U": - //publish action - return { - letter: ch, - labelKey: "buttons_saveAndPublish", - handler: args.methods.saveAndPublish, - hotKey: "ctrl+p", - hotKeyWhenHidden: true - }; - case "H": - //send to publish - return { - letter: ch, - labelKey: "buttons_saveToPublish", - handler: args.methods.sendToPublish, - hotKey: "ctrl+p", - hotKeyWhenHidden: true - }; - case "A": - //save - return { - letter: ch, - labelKey: "buttons_save", - handler: args.methods.save, - hotKey: "ctrl+s", - hotKeyWhenHidden: true - }; - case "Z": - //unpublish - return { - letter: ch, - labelKey: "content_unPublish", - handler: args.methods.unPublish, - hotKey: "ctrl+u", - hotKeyWhenHidden: true - }; - default: - return null; - } - } - - //reset - buttons.subButtons = []; - - //This is the ideal button order but depends on circumstance, we'll use this array to create the button list - // Publish, SendToPublish, Save - var buttonOrder = ["U", "H", "A"]; - - //Create the first button (primary button) - //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. - if (!args.create || _.contains(args.content.allowedActions, "C")) { - for (var b in buttonOrder) { - if (_.contains(args.content.allowedActions, buttonOrder[b])) { - buttons.defaultButton = createButtonDefinition(buttonOrder[b]); - break; - } - } - } - - //Now we need to make the drop down button list, this is also slightly tricky because: - //We cannot have any buttons if there's no default button above. - //We cannot have the unpublish button (Z) when there's no publish permission. - //We cannot have the unpublish button (Z) when the item is not published. - if (buttons.defaultButton) { - - //get the last index of the button order - var lastIndex = _.indexOf(buttonOrder, buttons.defaultButton.letter); - //add the remaining - for (var i = lastIndex + 1; i < buttonOrder.length; i++) { - if (_.contains(args.content.allowedActions, buttonOrder[i])) { - buttons.subButtons.push(createButtonDefinition(buttonOrder[i])); - } - } - - - //if we are not creating, then we should add unpublish too, - // so long as it's already published and if the user has access to publish - if (!args.create) { - if (args.content.publishDate && _.contains(args.content.allowedActions, "U")) { - buttons.subButtons.push(createButtonDefinition("Z")); - } - } - } - - return buttons; - }, - - /** + here we'll build the buttons according to the chars of the user. */ + configureContentEditorButtons: function (args) { + if (!angular.isObject(args)) { + throw 'args must be an object'; + } + if (!args.content) { + throw 'args.content is not defined'; + } + if (!args.methods) { + throw 'args.methods is not defined'; + } + if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.save || !args.methods.unPublish) { + throw 'args.methods does not contain all required defined methods'; + } + var buttons = { + defaultButton: null, + subButtons: [] + }; + function createButtonDefinition(ch) { + switch (ch) { + case 'U': + //publish action + return { + letter: ch, + labelKey: 'buttons_saveAndPublish', + handler: args.methods.saveAndPublish, + hotKey: 'ctrl+p', + hotKeyWhenHidden: true + }; + case 'H': + //send to publish + return { + letter: ch, + labelKey: 'buttons_saveToPublish', + handler: args.methods.sendToPublish, + hotKey: 'ctrl+p', + hotKeyWhenHidden: true + }; + case 'A': + //save + return { + letter: ch, + labelKey: 'buttons_save', + handler: args.methods.save, + hotKey: 'ctrl+s', + hotKeyWhenHidden: true + }; + case 'Z': + //unpublish + return { + letter: ch, + labelKey: 'content_unPublish', + handler: args.methods.unPublish, + hotKey: 'ctrl+u', + hotKeyWhenHidden: true + }; + default: + return null; + } + } + //reset + buttons.subButtons = []; + //This is the ideal button order but depends on circumstance, we'll use this array to create the button list + // Publish, SendToPublish, Save + var buttonOrder = [ + 'U', + 'H', + 'A' + ]; + //Create the first button (primary button) + //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. + //Another tricky rule is if they only have Create + Browse permissions but not Save but if it's being created then they will + // require the Save button in order to create. + //So this code is going to create the primary button (either Publish, SendToPublish, Save) if we are not in create mode + // or if the user has access to create. + if (!args.create || _.contains(args.content.allowedActions, 'C')) { + for (var b in buttonOrder) { + if (_.contains(args.content.allowedActions, buttonOrder[b])) { + buttons.defaultButton = createButtonDefinition(buttonOrder[b]); + break; + } + } + //Here's the special check, if the button still isn't set and we are creating and they have create access + //we need to add the Save button + if (!buttons.defaultButton && args.create && _.contains(args.content.allowedActions, 'C')) { + buttons.defaultButton = createButtonDefinition('A'); + } + } + //Now we need to make the drop down button list, this is also slightly tricky because: + //We cannot have any buttons if there's no default button above. + //We cannot have the unpublish button (Z) when there's no publish permission. + //We cannot have the unpublish button (Z) when the item is not published. + if (buttons.defaultButton) { + //get the last index of the button order + var lastIndex = _.indexOf(buttonOrder, buttons.defaultButton.letter); + //add the remaining + for (var i = lastIndex + 1; i < buttonOrder.length; i++) { + if (_.contains(args.content.allowedActions, buttonOrder[i])) { + buttons.subButtons.push(createButtonDefinition(buttonOrder[i])); + } + } + //if we are not creating, then we should add unpublish too, + // so long as it's already published and if the user has access to publish + if (!args.create) { + if (args.content.publishDate && _.contains(args.content.allowedActions, 'U')) { + buttons.subButtons.push(createButtonDefinition('Z')); + } + } + } + return buttons; + }, + /** * @ngdoc method * @name umbraco.services.contentEditingHelper#getAllProps * @methodOf umbraco.services.contentEditingHelper @@ -955,21 +898,17 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * * @description * Returns all propertes contained for the content item (since the normal model has properties contained inside of tabs) - */ - getAllProps: function (content) { - var allProps = []; - - for (var i = 0; i < content.tabs.length; i++) { - for (var p = 0; p < content.tabs[i].properties.length; p++) { - allProps.push(content.tabs[i].properties[p]); - } - } - - return allProps; - }, - - - /** + */ + getAllProps: function (content) { + var allProps = []; + for (var i = 0; i < content.tabs.length; i++) { + for (var p = 0; p < content.tabs[i].properties.length; p++) { + allProps.push(content.tabs[i].properties[p]); + } + } + return allProps; + }, + /** * @ngdoc method * @name umbraco.services.contentEditingHelper#configureButtons * @methodOf umbraco.services.contentEditingHelper @@ -977,56 +916,52 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * * @description * Returns a letter array for buttons, with the primary one first based on content model, permissions and editor state - */ - getAllowedActions : function(content, creating){ - - //This is the ideal button order but depends on circumstance, we'll use this array to create the button list - // Publish, SendToPublish, Save - var actionOrder = ["U", "H", "A"]; - var defaultActions; - var actions = []; - - //Create the first button (primary button) - //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. - if (!creating || _.contains(content.allowedActions, "C")) { - for (var b in actionOrder) { - if (_.contains(content.allowedActions, actionOrder[b])) { - defaultAction = actionOrder[b]; - break; - } - } - } - - actions.push(defaultAction); - - //Now we need to make the drop down button list, this is also slightly tricky because: - //We cannot have any buttons if there's no default button above. - //We cannot have the unpublish button (Z) when there's no publish permission. - //We cannot have the unpublish button (Z) when the item is not published. - if (defaultAction) { - //get the last index of the button order - var lastIndex = _.indexOf(actionOrder, defaultAction); - - //add the remaining - for (var i = lastIndex + 1; i < actionOrder.length; i++) { - if (_.contains(content.allowedActions, actionOrder[i])) { - actions.push(actionOrder[i]); - } - } - - //if we are not creating, then we should add unpublish too, - // so long as it's already published and if the user has access to publish - if (!creating) { - if (content.publishDate && _.contains(content.allowedActions,"U")) { - actions.push("Z"); - } - } - } - - return actions; - }, - - /** + */ + getAllowedActions: function (content, creating) { + //This is the ideal button order but depends on circumstance, we'll use this array to create the button list + // Publish, SendToPublish, Save + var actionOrder = [ + 'U', + 'H', + 'A' + ]; + var defaultActions; + var actions = []; + //Create the first button (primary button) + //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. + if (!creating || _.contains(content.allowedActions, 'C')) { + for (var b in actionOrder) { + if (_.contains(content.allowedActions, actionOrder[b])) { + defaultAction = actionOrder[b]; + break; + } + } + } + actions.push(defaultAction); + //Now we need to make the drop down button list, this is also slightly tricky because: + //We cannot have any buttons if there's no default button above. + //We cannot have the unpublish button (Z) when there's no publish permission. + //We cannot have the unpublish button (Z) when the item is not published. + if (defaultAction) { + //get the last index of the button order + var lastIndex = _.indexOf(actionOrder, defaultAction); + //add the remaining + for (var i = lastIndex + 1; i < actionOrder.length; i++) { + if (_.contains(content.allowedActions, actionOrder[i])) { + actions.push(actionOrder[i]); + } + } + //if we are not creating, then we should add unpublish too, + // so long as it's already published and if the user has access to publish + if (!creating) { + if (content.publishDate && _.contains(content.allowedActions, 'U')) { + actions.push('Z'); + } + } + } + return actions; + }, + /** * @ngdoc method * @name umbraco.services.contentEditingHelper#getButtonFromAction * @methodOf umbraco.services.contentEditingHelper @@ -1036,44 +971,42 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * Returns a button object to render a button for the tabbed editor * currently only returns built in system buttons for content and media actions * returns label, alias, action char and hot-key - */ - getButtonFromAction : function(ch){ - switch (ch) { - case "U": - return { - letter: ch, - labelKey: "buttons_saveAndPublish", - handler: "saveAndPublish", - hotKey: "ctrl+p" - }; - case "H": - //send to publish - return { - letter: ch, - labelKey: "buttons_saveToPublish", - handler: "sendToPublish", - hotKey: "ctrl+p" - }; - case "A": - return { - letter: ch, - labelKey: "buttons_save", - handler: "save", - hotKey: "ctrl+s" - }; - case "Z": - return { - letter: ch, - labelKey: "content_unPublish", - handler: "unPublish" - }; - - default: - return null; - } - - }, - /** + */ + getButtonFromAction: function (ch) { + switch (ch) { + case 'U': + return { + letter: ch, + labelKey: 'buttons_saveAndPublish', + handler: 'saveAndPublish', + hotKey: 'ctrl+p' + }; + case 'H': + //send to publish + return { + letter: ch, + labelKey: 'buttons_saveToPublish', + handler: 'sendToPublish', + hotKey: 'ctrl+p' + }; + case 'A': + return { + letter: ch, + labelKey: 'buttons_save', + handler: 'save', + hotKey: 'ctrl+s' + }; + case 'Z': + return { + letter: ch, + labelKey: 'content_unPublish', + handler: 'unPublish' + }; + default: + return null; + } + }, + /** * @ngdoc method * @name umbraco.services.contentEditingHelper#reBindChangedProperties * @methodOf umbraco.services.contentEditingHelper @@ -1081,65 +1014,59 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * * @description * re-binds all changed property values to the origContent object from the savedContent object and returns an array of changed properties. - */ - reBindChangedProperties: function (origContent, savedContent) { - - var changed = []; - - //get a list of properties since they are contained in tabs - var allOrigProps = this.getAllProps(origContent); - var allNewProps = this.getAllProps(savedContent); - - function getNewProp(alias) { - return _.find(allNewProps, function (item) { - return item.alias === alias; - }); - } - - //a method to ignore built-in prop changes - var shouldIgnore = function(propName) { - return _.some(["tabs", "notifications", "ModelState", "tabs", "properties"], function(i) { - return i === propName; - }); - }; - //check for changed built-in properties of the content - for (var o in origContent) { - - //ignore the ones listed in the array - if (shouldIgnore(o)) { - continue; - } - - if (!_.isEqual(origContent[o], savedContent[o])) { - origContent[o] = savedContent[o]; - } - } - - //check for changed properties of the content - for (var p in allOrigProps) { - var newProp = getNewProp(allOrigProps[p].alias); - if (newProp && !_.isEqual(allOrigProps[p].value, newProp.value)) { - - //they have changed so set the origContent prop to the new one - var origVal = allOrigProps[p].value; - allOrigProps[p].value = newProp.value; - - //instead of having a property editor $watch their expression to check if it has - // been updated, instead we'll check for the existence of a special method on their model - // and just call it. - if (angular.isFunction(allOrigProps[p].onValueChanged)) { - //send the newVal + oldVal - allOrigProps[p].onValueChanged(allOrigProps[p].value, origVal); - } - - changed.push(allOrigProps[p]); - } - } - - return changed; - }, - - /** + */ + reBindChangedProperties: function (origContent, savedContent) { + var changed = []; + //get a list of properties since they are contained in tabs + var allOrigProps = this.getAllProps(origContent); + var allNewProps = this.getAllProps(savedContent); + function getNewProp(alias) { + return _.find(allNewProps, function (item) { + return item.alias === alias; + }); + } + //a method to ignore built-in prop changes + var shouldIgnore = function (propName) { + return _.some([ + 'tabs', + 'notifications', + 'ModelState', + 'tabs', + 'properties' + ], function (i) { + return i === propName; + }); + }; + //check for changed built-in properties of the content + for (var o in origContent) { + //ignore the ones listed in the array + if (shouldIgnore(o)) { + continue; + } + if (!_.isEqual(origContent[o], savedContent[o])) { + origContent[o] = savedContent[o]; + } + } + //check for changed properties of the content + for (var p in allOrigProps) { + var newProp = getNewProp(allOrigProps[p].alias); + if (newProp && !_.isEqual(allOrigProps[p].value, newProp.value)) { + //they have changed so set the origContent prop to the new one + var origVal = allOrigProps[p].value; + allOrigProps[p].value = newProp.value; + //instead of having a property editor $watch their expression to check if it has + // been updated, instead we'll check for the existence of a special method on their model + // and just call it. + if (angular.isFunction(allOrigProps[p].onValueChanged)) { + //send the newVal + oldVal + allOrigProps[p].onValueChanged(allOrigProps[p].value, origVal); + } + changed.push(allOrigProps[p]); + } + } + return changed; + }, + /** * @ngdoc function * @name umbraco.services.contentEditingHelper#handleSaveError * @methodOf umbraco.services.contentEditingHelper @@ -1147,53 +1074,38 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * * @description * A function to handle what happens when we have validation issues from the server side - */ - handleSaveError: function (args) { - - if (!args.err) { - throw "args.err cannot be null"; - } - if (args.redirectOnFailure === undefined || args.redirectOnFailure === null) { - throw "args.redirectOnFailure must be set to true or false"; - } - - //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors. - //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something). - //Or, some strange server error - if (args.err.status === 400) { - //now we need to look through all the validation errors - if (args.err.data && (args.err.data.ModelState)) { - - //wire up the server validation errs - formHelper.handleServerValidation(args.err.data.ModelState); - - if (!args.redirectOnFailure || !this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) { - //we are not redirecting because this is not new content, it is existing content. In this case - // we need to detect what properties have changed and re-bind them with the server data. Then we need - // to re-bind any server validation errors after the digest takes place. - - if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { - args.rebindCallback(); - } - - serverValidationManager.executeAndClearAllSubscriptions(); - } - - //indicates we've handled the server result - return true; - } - else { - dialogService.ysodDialog(args.err); - } - } - else { - dialogService.ysodDialog(args.err); - } - - return false; - }, - - /** + */ + handleSaveError: function (args) { + if (!args.err) { + throw 'args.err cannot be null'; + } + if (args.redirectOnFailure === undefined || args.redirectOnFailure === null) { + throw 'args.redirectOnFailure must be set to true or false'; + } + //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors. + //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something). + //Or, some strange server error + if (args.err.status === 400) { + //now we need to look through all the validation errors + if (args.err.data && args.err.data.ModelState) { + //wire up the server validation errs + formHelper.handleServerValidation(args.err.data.ModelState); + if (!args.redirectOnFailure || !this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) { + //we are not redirecting because this is not new content, it is existing content. In this case + // we need to detect what properties have changed and re-bind them with the server data. Then we need + // to re-bind any server validation errors after the digest takes place. + if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { + args.rebindCallback(); + } + serverValidationManager.executeAndClearAllSubscriptions(); + } + //indicates we've handled the server result + return true; + } + } + return false; + }, + /** * @ngdoc function * @name umbraco.services.contentEditingHelper#handleSuccessfulSave * @methodOf umbraco.services.contentEditingHelper @@ -1203,28 +1115,24 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * A function to handle when saving a content item is successful. This will rebind the values of the model that have changed * ensure the notifications are displayed and that the appropriate events are fired. This will also check if we need to redirect * when we're creating new content. - */ - handleSuccessfulSave: function (args) { - - if (!args) { - throw "args cannot be null"; - } - if (!args.savedContent) { - throw "args.savedContent cannot be null"; - } - - if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id)) { - - //we are not redirecting because this is not new content, it is existing content. In this case - // we need to detect what properties have changed and re-bind them with the server data. - //call the callback - if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { - args.rebindCallback(); - } - } - }, - - /** + */ + handleSuccessfulSave: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.savedContent) { + throw 'args.savedContent cannot be null'; + } + if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id)) { + //we are not redirecting because this is not new content, it is existing content. In this case + // we need to detect what properties have changed and re-bind them with the server data. + //call the callback + if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { + args.rebindCallback(); + } + } + }, + /** * @ngdoc function * @name umbraco.services.contentEditingHelper#redirectToCreatedContent * @methodOf umbraco.services.contentEditingHelper @@ -1234,31 +1142,26 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * Changes the location to be editing the newly created content after create was successful. * We need to decide if we need to redirect to edito mode or if we will remain in create mode. * We will only need to maintain create mode if we have not fulfilled the basic requirements for creating an entity which is at least having a name and ID - */ - redirectToCreatedContent: function (id, modelState) { - - //only continue if we are currently in create mode and if there is no 'Name' modelstate errors - // since we need at least a name to create content. - if ($routeParams.create && (isValidIdentifier(id) && (!modelState || !modelState["Name"]))) { - - //need to change the location to not be in 'create' mode. Currently the route will be something like: - // /belle/#/content/edit/1234?doctype=newsArticle&create=true - // but we need to remove everything after the query so that it is just: - // /belle/#/content/edit/9876 (where 9876 is the new id) - - //clear the query strings - $location.search(""); - - //change to new path - $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); - //don't add a browser history for this - $location.replace(); - return true; - } - return false; - }, - - /** + */ + redirectToCreatedContent: function (id, modelState) { + //only continue if we are currently in create mode and if there is no 'Name' modelstate errors + // since we need at least a name to create content. + if ($routeParams.create && (isValidIdentifier(id) && (!modelState || !modelState['Name']))) { + //need to change the location to not be in 'create' mode. Currently the route will be something like: + // /belle/#/content/edit/1234?doctype=newsArticle&create=true + // but we need to remove everything after the query so that it is just: + // /belle/#/content/edit/9876 (where 9876 is the new id) + //clear the query strings + $location.search(''); + //change to new path + $location.path('/' + $routeParams.section + '/' + $routeParams.tree + '/' + $routeParams.method + '/' + id); + //don't add a browser history for this + $location.replace(); + return true; + } + return false; + }, + /** * @ngdoc function * @name umbraco.services.contentEditingHelper#redirectToRenamedContent * @methodOf umbraco.services.contentEditingHelper @@ -1267,599 +1170,462 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * @description * For some editors like scripts or entites that have names as ids, these names can change and we need to redirect * to their new paths, this is helper method to do that. - */ - redirectToRenamedContent: function (id) { - //clear the query strings - $location.search(""); - //change to new path - $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); - //don't add a browser history for this - $location.replace(); - return true; - } - }; -} -angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper); - -/** - * @ngdoc service - * @name umbraco.services.contentTypeHelper - * @description A helper service for the content type editor - **/ -function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $injector, $q) { - - var contentTypeHelperService = { - - createIdArray: function(array) { - - var newArray = []; - - angular.forEach(array, function(arrayItem){ - - if(angular.isObject(arrayItem)) { - newArray.push(arrayItem.id); - } else { - newArray.push(arrayItem); - } - - }); - - return newArray; - - }, - - generateModels: function () { - var deferred = $q.defer(); - var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; - var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled; - if (modelsBuilderEnabled && modelsResource) { - modelsResource.buildModels().then(function(result) { - deferred.resolve(result); - - //just calling this to get the servar back to life - modelsResource.getModelsOutOfDateStatus(); - - }, function(e) { - deferred.reject(e); - }); - } - else { - deferred.resolve(false); - } - return deferred.promise; - }, - - checkModelsBuilderStatus: function () { - var deferred = $q.defer(); - var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; - var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true); - - if (modelsBuilderEnabled && modelsResource) { - modelsResource.getModelsOutOfDateStatus().then(function(result) { - //Generate models buttons should be enabled if it is 0 - deferred.resolve(result.status === 0); - }); - } - else { - deferred.resolve(false); + */ + redirectToRenamedContent: function (id) { + //clear the query strings + $location.search(''); + //change to new path + $location.path('/' + $routeParams.section + '/' + $routeParams.tree + '/' + $routeParams.method + '/' + id); + //don't add a browser history for this + $location.replace(); + return true; } - return deferred.promise; - }, - - makeObjectArrayFromId: function (idArray, objectArray) { - var newArray = []; - - for (var idIndex = 0; idArray.length > idIndex; idIndex++) { - var id = idArray[idIndex]; - - for (var objectIndex = 0; objectArray.length > objectIndex; objectIndex++) { - var object = objectArray[objectIndex]; - if (id === object.id) { - newArray.push(object); - } - } - - } - - return newArray; - }, - - validateAddingComposition: function(contentType, compositeContentType) { - - //Validate that by adding this group that we are not adding duplicate property type aliases - - var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function(g) { - return _.map(g.properties, function(p) { - return p.alias; - }); - })); - var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function(g) { - return _.map(g.properties, function(p) { - return p.alias; - }); - })), function(f) { - return f !== null && f !== undefined; - }); - - var intersec = _.intersection(propertiesAdding, propAliasesExisting); - if (intersec.length > 0) { - //return the overlapping property aliases - return intersec; - } - - //no overlapping property aliases - return []; - }, - - mergeCompositeContentType: function(contentType, compositeContentType) { - - //Validate that there are no overlapping aliases - var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType); - if (overlappingAliases.length > 0) { - throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join()); - } - - angular.forEach(compositeContentType.groups, function(compositionGroup) { - - // order composition groups based on sort order - compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder'); - - // get data type details - angular.forEach(compositionGroup.properties, function(property) { - dataTypeResource.getById(property.dataTypeId) - .then(function(dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - }); - - // set inherited state on tab - compositionGroup.inherited = true; - - // set inherited state on properties - angular.forEach(compositionGroup.properties, function(compositionProperty) { - compositionProperty.inherited = true; - }); - - // set tab state - compositionGroup.tabState = "inActive"; - - // if groups are named the same - merge the groups - angular.forEach(contentType.groups, function(contentTypeGroup) { - - if (contentTypeGroup.name === compositionGroup.name) { - - // set flag to show if properties has been merged into a tab - compositionGroup.groupIsMerged = true; - - // make group inherited - contentTypeGroup.inherited = true; - - // add properties to the top of the array - contentTypeGroup.properties = compositionGroup.properties.concat(contentTypeGroup.properties); - - // update sort order on all properties in merged group - contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); - - // make parentTabContentTypeNames to an array so we can push values - if (contentTypeGroup.parentTabContentTypeNames === null || contentTypeGroup.parentTabContentTypeNames === undefined) { - contentTypeGroup.parentTabContentTypeNames = []; - } - - // push name to array of merged composite content types - contentTypeGroup.parentTabContentTypeNames.push(compositeContentType.name); - - // make parentTabContentTypes to an array so we can push values - if (contentTypeGroup.parentTabContentTypes === null || contentTypeGroup.parentTabContentTypes === undefined) { - contentTypeGroup.parentTabContentTypes = []; - } - - // push id to array of merged composite content types - contentTypeGroup.parentTabContentTypes.push(compositeContentType.id); - - // get sort order from composition - contentTypeGroup.sortOrder = compositionGroup.sortOrder; - - // splice group to the top of the array - var contentTypeGroupCopy = angular.copy(contentTypeGroup); - var index = contentType.groups.indexOf(contentTypeGroup); - contentType.groups.splice(index, 1); - contentType.groups.unshift(contentTypeGroupCopy); - - } - - }); - - // if group is not merged - push it to the end of the array - before init tab - if (compositionGroup.groupIsMerged === false || compositionGroup.groupIsMerged === undefined) { - - // make parentTabContentTypeNames to an array so we can push values - if (compositionGroup.parentTabContentTypeNames === null || compositionGroup.parentTabContentTypeNames === undefined) { - compositionGroup.parentTabContentTypeNames = []; - } - - // push name to array of merged composite content types - compositionGroup.parentTabContentTypeNames.push(compositeContentType.name); - - // make parentTabContentTypes to an array so we can push values - if (compositionGroup.parentTabContentTypes === null || compositionGroup.parentTabContentTypes === undefined) { - compositionGroup.parentTabContentTypes = []; - } - - // push id to array of merged composite content types - compositionGroup.parentTabContentTypes.push(compositeContentType.id); - - // push group before placeholder tab - contentType.groups.unshift(compositionGroup); - - } - - }); - - // sort all groups by sortOrder property - contentType.groups = $filter('orderBy')(contentType.groups, 'sortOrder'); - - return contentType; - - }, - - splitCompositeContentType: function (contentType, compositeContentType) { - - var groups = []; - - angular.forEach(contentType.groups, function(contentTypeGroup){ - - if( contentTypeGroup.tabState !== "init" ) { - - var idIndex = contentTypeGroup.parentTabContentTypes.indexOf(compositeContentType.id); - var nameIndex = contentTypeGroup.parentTabContentTypeNames.indexOf(compositeContentType.name); - var groupIndex = contentType.groups.indexOf(contentTypeGroup); - - - if( idIndex !== -1 ) { - - var properties = []; - - // remove all properties from composite content type - angular.forEach(contentTypeGroup.properties, function(property){ - if(property.contentTypeId !== compositeContentType.id) { - properties.push(property); - } - }); - - // set new properties array to properties - contentTypeGroup.properties = properties; - - // remove composite content type name and id from inherited arrays - contentTypeGroup.parentTabContentTypes.splice(idIndex, 1); - contentTypeGroup.parentTabContentTypeNames.splice(nameIndex, 1); - - // remove inherited state if there are no inherited properties - if(contentTypeGroup.parentTabContentTypes.length === 0) { - contentTypeGroup.inherited = false; - } - - // remove group if there are no properties left - if(contentTypeGroup.properties.length > 1) { - //contentType.groups.splice(groupIndex, 1); - groups.push(contentTypeGroup); - } - + }; + } + angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper); + /** + * @ngdoc service + * @name umbraco.services.contentTypeHelper + * @description A helper service for the content type editor + **/ + function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $injector, $q) { + var contentTypeHelperService = { + createIdArray: function (array) { + var newArray = []; + angular.forEach(array, function (arrayItem) { + if (angular.isObject(arrayItem)) { + newArray.push(arrayItem.id); } else { - groups.push(contentTypeGroup); + newArray.push(arrayItem); } - - } else { - groups.push(contentTypeGroup); - } - - // update sort order on properties - contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); - - }); - - contentType.groups = groups; - - }, - - updatePropertiesSortOrder: function (properties) { - - var sortOrder = 0; - - angular.forEach(properties, function(property) { - if( !property.inherited && property.propertyState !== "init") { - property.sortOrder = sortOrder; - } - sortOrder++; - }); - - return properties; - - }, - - getTemplatePlaceholder: function() { - - var templatePlaceholder = { - "name": "", - "icon": "icon-layout", - "alias": "templatePlaceholder", - "placeholder": true - }; - - return templatePlaceholder; - - }, - - insertDefaultTemplatePlaceholder: function(defaultTemplate) { - - // get template placeholder - var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); - - // add as default template - defaultTemplate = templatePlaceholder; - - return defaultTemplate; - - }, - - insertTemplatePlaceholder: function(array) { - - // get template placeholder - var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); - - // add as selected item - array.push(templatePlaceholder); - - return array; - - }, - - insertChildNodePlaceholder: function (array, name, icon, id) { - - var placeholder = { - "name": name, - "icon": icon, - "id": id - }; - - array.push(placeholder); - - } - - }; - - return contentTypeHelperService; -} -angular.module('umbraco.services').factory('contentTypeHelper', contentTypeHelper); - -/** -* @ngdoc service -* @name umbraco.services.cropperHelper -* @description A helper object used for dealing with image cropper data + }); + return newArray; + }, + generateModels: function () { + var deferred = $q.defer(); + var modelsResource = $injector.has('modelsBuilderResource') ? $injector.get('modelsBuilderResource') : null; + var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled; + if (modelsBuilderEnabled && modelsResource) { + modelsResource.buildModels().then(function (result) { + deferred.resolve(result); + //just calling this to get the servar back to life + modelsResource.getModelsOutOfDateStatus(); + }, function (e) { + deferred.reject(e); + }); + } else { + deferred.resolve(false); + } + return deferred.promise; + }, + checkModelsBuilderStatus: function () { + var deferred = $q.defer(); + var modelsResource = $injector.has('modelsBuilderResource') ? $injector.get('modelsBuilderResource') : null; + var modelsBuilderEnabled = Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true; + if (modelsBuilderEnabled && modelsResource) { + modelsResource.getModelsOutOfDateStatus().then(function (result) { + //Generate models buttons should be enabled if it is 0 + deferred.resolve(result.status === 0); + }); + } else { + deferred.resolve(false); + } + return deferred.promise; + }, + makeObjectArrayFromId: function (idArray, objectArray) { + var newArray = []; + for (var idIndex = 0; idArray.length > idIndex; idIndex++) { + var id = idArray[idIndex]; + for (var objectIndex = 0; objectArray.length > objectIndex; objectIndex++) { + var object = objectArray[objectIndex]; + if (id === object.id) { + newArray.push(object); + } + } + } + return newArray; + }, + validateAddingComposition: function (contentType, compositeContentType) { + //Validate that by adding this group that we are not adding duplicate property type aliases + var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function (g) { + return _.map(g.properties, function (p) { + return p.alias; + }); + })); + var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function (g) { + return _.map(g.properties, function (p) { + return p.alias; + }); + })), function (f) { + return f !== null && f !== undefined; + }); + var intersec = _.intersection(propertiesAdding, propAliasesExisting); + if (intersec.length > 0) { + //return the overlapping property aliases + return intersec; + } + //no overlapping property aliases + return []; + }, + mergeCompositeContentType: function (contentType, compositeContentType) { + //Validate that there are no overlapping aliases + var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType); + if (overlappingAliases.length > 0) { + throw new Error('Cannot add this composition, these properties already exist on the content type: ' + overlappingAliases.join()); + } + angular.forEach(compositeContentType.groups, function (compositionGroup) { + // order composition groups based on sort order + compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder'); + // get data type details + angular.forEach(compositionGroup.properties, function (property) { + dataTypeResource.getById(property.dataTypeId).then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + }); + // set inherited state on tab + compositionGroup.inherited = true; + // set inherited state on properties + angular.forEach(compositionGroup.properties, function (compositionProperty) { + compositionProperty.inherited = true; + }); + // set tab state + compositionGroup.tabState = 'inActive'; + // if groups are named the same - merge the groups + angular.forEach(contentType.groups, function (contentTypeGroup) { + if (contentTypeGroup.name === compositionGroup.name) { + // set flag to show if properties has been merged into a tab + compositionGroup.groupIsMerged = true; + // make group inherited + contentTypeGroup.inherited = true; + // add properties to the top of the array + contentTypeGroup.properties = compositionGroup.properties.concat(contentTypeGroup.properties); + // update sort order on all properties in merged group + contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); + // make parentTabContentTypeNames to an array so we can push values + if (contentTypeGroup.parentTabContentTypeNames === null || contentTypeGroup.parentTabContentTypeNames === undefined) { + contentTypeGroup.parentTabContentTypeNames = []; + } + // push name to array of merged composite content types + contentTypeGroup.parentTabContentTypeNames.push(compositeContentType.name); + // make parentTabContentTypes to an array so we can push values + if (contentTypeGroup.parentTabContentTypes === null || contentTypeGroup.parentTabContentTypes === undefined) { + contentTypeGroup.parentTabContentTypes = []; + } + // push id to array of merged composite content types + contentTypeGroup.parentTabContentTypes.push(compositeContentType.id); + // get sort order from composition + contentTypeGroup.sortOrder = compositionGroup.sortOrder; + // splice group to the top of the array + var contentTypeGroupCopy = angular.copy(contentTypeGroup); + var index = contentType.groups.indexOf(contentTypeGroup); + contentType.groups.splice(index, 1); + contentType.groups.unshift(contentTypeGroupCopy); + } + }); + // if group is not merged - push it to the end of the array - before init tab + if (compositionGroup.groupIsMerged === false || compositionGroup.groupIsMerged === undefined) { + // make parentTabContentTypeNames to an array so we can push values + if (compositionGroup.parentTabContentTypeNames === null || compositionGroup.parentTabContentTypeNames === undefined) { + compositionGroup.parentTabContentTypeNames = []; + } + // push name to array of merged composite content types + compositionGroup.parentTabContentTypeNames.push(compositeContentType.name); + // make parentTabContentTypes to an array so we can push values + if (compositionGroup.parentTabContentTypes === null || compositionGroup.parentTabContentTypes === undefined) { + compositionGroup.parentTabContentTypes = []; + } + // push id to array of merged composite content types + compositionGroup.parentTabContentTypes.push(compositeContentType.id); + // push group before placeholder tab + contentType.groups.unshift(compositionGroup); + } + }); + // sort all groups by sortOrder property + contentType.groups = $filter('orderBy')(contentType.groups, 'sortOrder'); + return contentType; + }, + splitCompositeContentType: function (contentType, compositeContentType) { + var groups = []; + angular.forEach(contentType.groups, function (contentTypeGroup) { + if (contentTypeGroup.tabState !== 'init') { + var idIndex = contentTypeGroup.parentTabContentTypes.indexOf(compositeContentType.id); + var nameIndex = contentTypeGroup.parentTabContentTypeNames.indexOf(compositeContentType.name); + var groupIndex = contentType.groups.indexOf(contentTypeGroup); + if (idIndex !== -1) { + var properties = []; + // remove all properties from composite content type + angular.forEach(contentTypeGroup.properties, function (property) { + if (property.contentTypeId !== compositeContentType.id) { + properties.push(property); + } + }); + // set new properties array to properties + contentTypeGroup.properties = properties; + // remove composite content type name and id from inherited arrays + contentTypeGroup.parentTabContentTypes.splice(idIndex, 1); + contentTypeGroup.parentTabContentTypeNames.splice(nameIndex, 1); + // remove inherited state if there are no inherited properties + if (contentTypeGroup.parentTabContentTypes.length === 0) { + contentTypeGroup.inherited = false; + } + // remove group if there are no properties left + if (contentTypeGroup.properties.length > 1) { + //contentType.groups.splice(groupIndex, 1); + groups.push(contentTypeGroup); + } + } else { + groups.push(contentTypeGroup); + } + } else { + groups.push(contentTypeGroup); + } + // update sort order on properties + contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); + }); + contentType.groups = groups; + }, + updatePropertiesSortOrder: function (properties) { + var sortOrder = 0; + angular.forEach(properties, function (property) { + if (!property.inherited && property.propertyState !== 'init') { + property.sortOrder = sortOrder; + } + sortOrder++; + }); + return properties; + }, + getTemplatePlaceholder: function () { + var templatePlaceholder = { + 'name': '', + 'icon': 'icon-layout', + 'alias': 'templatePlaceholder', + 'placeholder': true + }; + return templatePlaceholder; + }, + insertDefaultTemplatePlaceholder: function (defaultTemplate) { + // get template placeholder + var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); + // add as default template + defaultTemplate = templatePlaceholder; + return defaultTemplate; + }, + insertTemplatePlaceholder: function (array) { + // get template placeholder + var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); + // add as selected item + array.push(templatePlaceholder); + return array; + }, + insertChildNodePlaceholder: function (array, name, icon, id) { + var placeholder = { + 'name': name, + 'icon': icon, + 'id': id + }; + array.push(placeholder); + } + }; + return contentTypeHelperService; + } + angular.module('umbraco.services').factory('contentTypeHelper', contentTypeHelper); + /** +* @ngdoc service +* @name umbraco.services.cropperHelper +* @description A helper object used for dealing with image cropper data **/ -function cropperHelper(umbRequestHelper, $http) { - var service = { - - /** - * @ngdoc method - * @name umbraco.services.cropperHelper#configuration - * @methodOf umbraco.services.cropperHelper - * - * @description - * Returns a collection of plugins available to the tinyMCE editor - * + function cropperHelper(umbRequestHelper, $http) { + var service = { + /** + * @ngdoc method + * @name umbraco.services.cropperHelper#configuration + * @methodOf umbraco.services.cropperHelper + * + * @description + * Returns a collection of plugins available to the tinyMCE editor + * */ - configuration: function (mediaTypeAlias) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "imageCropperApiBaseUrl", - "GetConfiguration", - [{ mediaTypeAlias: mediaTypeAlias}])), - 'Failed to retrieve tinymce configuration'); - }, - - - //utill for getting either min/max aspect ratio to scale image after - calculateAspectRatioFit : function(srcWidth, srcHeight, maxWidth, maxHeight, maximize) { - var ratio = [maxWidth / srcWidth, maxHeight / srcHeight ]; - - if(maximize){ - ratio = Math.max(ratio[0], ratio[1]); - }else{ - ratio = Math.min(ratio[0], ratio[1]); - } - - return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio}; - }, - - //utill for scaling width / height given a ratio - calculateSizeToRatio : function(srcWidth, srcHeight, ratio) { - return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio}; - }, - - scaleToMaxSize : function(srcWidth, srcHeight, maxSize) { - - var retVal = {height: srcHeight, width: srcWidth}; - - if(srcWidth > maxSize ||srcHeight > maxSize){ - var ratio = [maxSize / srcWidth, maxSize / srcHeight ]; - ratio = Math.min(ratio[0], ratio[1]); - - retVal.height = srcHeight * ratio; - retVal.width = srcWidth * ratio; - } - - return retVal; - }, - - //returns a ng-style object with top,left,width,height pixel measurements - //expects {left,right,top,bottom} - {width,height}, {width,height}, int - //offset is just to push the image position a number of pixels from top,left - convertToStyle : function(coordinates, originalSize, viewPort, offset){ - - var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset); - var _offset = offset || 0; - - var x = 1 - (coordinates.x1 + Math.abs(coordinates.x2)); - var left_of_x = originalSize.width * x; - var ratio = viewPort.width / left_of_x; - - var style = { - position: "absolute", - top: -(coordinates_px.y1*ratio)+ _offset, - left: -(coordinates_px.x1* ratio)+ _offset, - width: Math.floor(originalSize.width * ratio), - height: Math.floor(originalSize.height * ratio), - originalWidth: originalSize.width, - originalHeight: originalSize.height, - ratio: ratio - }; - - return style; - }, - - - coordinatesToPixels : function(coordinates, originalSize, offset){ - - var coordinates_px = { - x1: Math.floor(coordinates.x1 * originalSize.width), - y1: Math.floor(coordinates.y1 * originalSize.height), - x2: Math.floor(coordinates.x2 * originalSize.width), - y2: Math.floor(coordinates.y2 * originalSize.height) - }; - - return coordinates_px; - }, - - pixelsToCoordinates : function(image, width, height, offset){ - - var x1_px = Math.abs(image.left-offset); - var y1_px = Math.abs(image.top-offset); - - var x2_px = image.width - (x1_px + width); - var y2_px = image.height - (y1_px + height); - - - //crop coordinates in % - var crop = {}; - crop.x1 = x1_px / image.width; - crop.y1 = y1_px / image.height; - crop.x2 = x2_px / image.width; - crop.y2 = y2_px / image.height; - - for(var coord in crop){ - if(crop[coord] < 0){ - crop[coord] = 0; - } - } - - return crop; - }, - - centerInsideViewPort : function(img, viewport){ - var left = viewport.width/ 2 - img.width / 2, - top = viewport.height / 2 - img.height / 2; - - return {left: left, top: top}; - }, - - alignToCoordinates : function(image, center, viewport){ - - var min_left = (image.width) - (viewport.width); - var min_top = (image.height) - (viewport.height); - - var c_top = -(center.top * image.height) + (viewport.height / 2); - var c_left = -(center.left * image.width) + (viewport.width / 2); - - if(c_top < -min_top){ - c_top = -min_top; - } - if(c_top > 0){ - c_top = 0; - } - if(c_left < -min_left){ - c_left = -min_left; - } - if(c_left > 0){ - c_left = 0; - } - return {left: c_left, top: c_top}; - }, - - - syncElements : function(source, target){ - target.height(source.height()); - target.width(source.width()); - - target.css({ - "top": source[0].offsetTop, - "left": source[0].offsetLeft - }); - } - }; - - return service; -} - -angular.module('umbraco.services').factory('cropperHelper', cropperHelper); - -/** - * @ngdoc service - * @name umbraco.services.dataTypeHelper - * @description A helper service for data types - **/ -function dataTypeHelper() { - - var dataTypeHelperService = { - - createPreValueProps: function(preVals) { - - var preValues = []; - - for (var i = 0; i < preVals.length; i++) { - preValues.push({ - hideLabel: preVals[i].hideLabel, - alias: preVals[i].key, - description: preVals[i].description, - label: preVals[i].label, - view: preVals[i].view, - value: preVals[i].value + configuration: function (mediaTypeAlias) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('imageCropperApiBaseUrl', 'GetConfiguration', [{ mediaTypeAlias: mediaTypeAlias }])), 'Failed to retrieve tinymce configuration'); + }, + //utill for getting either min/max aspect ratio to scale image after + calculateAspectRatioFit: function (srcWidth, srcHeight, maxWidth, maxHeight, maximize) { + var ratio = [ + maxWidth / srcWidth, + maxHeight / srcHeight + ]; + if (maximize) { + ratio = Math.max(ratio[0], ratio[1]); + } else { + ratio = Math.min(ratio[0], ratio[1]); + } + return { + width: srcWidth * ratio, + height: srcHeight * ratio, + ratio: ratio + }; + }, + //utill for scaling width / height given a ratio + calculateSizeToRatio: function (srcWidth, srcHeight, ratio) { + return { + width: srcWidth * ratio, + height: srcHeight * ratio, + ratio: ratio + }; + }, + scaleToMaxSize: function (srcWidth, srcHeight, maxSize) { + var retVal = { + height: srcHeight, + width: srcWidth + }; + if (srcWidth > maxSize || srcHeight > maxSize) { + var ratio = [ + maxSize / srcWidth, + maxSize / srcHeight + ]; + ratio = Math.min(ratio[0], ratio[1]); + retVal.height = srcHeight * ratio; + retVal.width = srcWidth * ratio; + } + return retVal; + }, + //returns a ng-style object with top,left,width,height pixel measurements + //expects {left,right,top,bottom} - {width,height}, {width,height}, int + //offset is just to push the image position a number of pixels from top,left + convertToStyle: function (coordinates, originalSize, viewPort, offset) { + var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset); + var _offset = offset || 0; + var x = 1 - (coordinates.x1 + Math.abs(coordinates.x2)); + var left_of_x = originalSize.width * x; + var ratio = viewPort.width / left_of_x; + var style = { + position: 'absolute', + top: -(coordinates_px.y1 * ratio) + _offset, + left: -(coordinates_px.x1 * ratio) + _offset, + width: Math.floor(originalSize.width * ratio), + height: Math.floor(originalSize.height * ratio), + originalWidth: originalSize.width, + originalHeight: originalSize.height, + ratio: ratio + }; + return style; + }, + coordinatesToPixels: function (coordinates, originalSize, offset) { + var coordinates_px = { + x1: Math.floor(coordinates.x1 * originalSize.width), + y1: Math.floor(coordinates.y1 * originalSize.height), + x2: Math.floor(coordinates.x2 * originalSize.width), + y2: Math.floor(coordinates.y2 * originalSize.height) + }; + return coordinates_px; + }, + pixelsToCoordinates: function (image, width, height, offset) { + var x1_px = Math.abs(image.left - offset); + var y1_px = Math.abs(image.top - offset); + var x2_px = image.width - (x1_px + width); + var y2_px = image.height - (y1_px + height); + //crop coordinates in % + var crop = {}; + crop.x1 = x1_px / image.width; + crop.y1 = y1_px / image.height; + crop.x2 = x2_px / image.width; + crop.y2 = y2_px / image.height; + for (var coord in crop) { + if (crop[coord] < 0) { + crop[coord] = 0; + } + } + return crop; + }, + centerInsideViewPort: function (img, viewport) { + var left = viewport.width / 2 - img.width / 2, top = viewport.height / 2 - img.height / 2; + return { + left: left, + top: top + }; + }, + alignToCoordinates: function (image, center, viewport) { + var min_left = image.width - viewport.width; + var min_top = image.height - viewport.height; + var c_top = -(center.top * image.height) + viewport.height / 2; + var c_left = -(center.left * image.width) + viewport.width / 2; + if (c_top < -min_top) { + c_top = -min_top; + } + if (c_top > 0) { + c_top = 0; + } + if (c_left < -min_left) { + c_left = -min_left; + } + if (c_left > 0) { + c_left = 0; + } + return { + left: c_left, + top: c_top + }; + }, + syncElements: function (source, target) { + target.height(source.height()); + target.width(source.width()); + target.css({ + 'top': source[0].offsetTop, + 'left': source[0].offsetLeft }); } - - return preValues; - - }, - - rebindChangedProperties: function (origContent, savedContent) { - - //a method to ignore built-in prop changes - var shouldIgnore = function (propName) { - return _.some(["notifications", "ModelState"], function (i) { - return i === propName; - }); - }; - //check for changed built-in properties of the content - for (var o in origContent) { - - //ignore the ones listed in the array - if (shouldIgnore(o)) { - continue; + }; + return service; + } + angular.module('umbraco.services').factory('cropperHelper', cropperHelper); + /** + * @ngdoc service + * @name umbraco.services.dataTypeHelper + * @description A helper service for data types + **/ + function dataTypeHelper() { + var dataTypeHelperService = { + createPreValueProps: function (preVals) { + var preValues = []; + for (var i = 0; i < preVals.length; i++) { + preValues.push({ + hideLabel: preVals[i].hideLabel, + alias: preVals[i].key, + description: preVals[i].description, + label: preVals[i].label, + view: preVals[i].view, + value: preVals[i].value + }); } - - if (!_.isEqual(origContent[o], savedContent[o])) { - origContent[o] = savedContent[o]; + return preValues; + }, + rebindChangedProperties: function (origContent, savedContent) { + //a method to ignore built-in prop changes + var shouldIgnore = function (propName) { + return _.some([ + 'notifications', + 'ModelState' + ], function (i) { + return i === propName; + }); + }; + //check for changed built-in properties of the content + for (var o in origContent) { + //ignore the ones listed in the array + if (shouldIgnore(o)) { + continue; + } + if (!_.isEqual(origContent[o], savedContent[o])) { + origContent[o] = savedContent[o]; + } } } - } - - }; - - return dataTypeHelperService; -} -angular.module('umbraco.services').factory('dataTypeHelper', dataTypeHelper); -/** + }; + return dataTypeHelperService; + } + angular.module('umbraco.services').factory('dataTypeHelper', dataTypeHelper); + /** * @ngdoc service * @name umbraco.services.dialogService * @@ -1887,255 +1653,204 @@ angular.module('umbraco.services').factory('dataTypeHelper', dataTypeHelper); * //data contains whatever the dialog has selected / attached * } * - */ - -angular.module('umbraco.services') -.factory('dialogService', function ($rootScope, $compile, $http, $timeout, $q, $templateCache, appState, eventsService) { - - var dialogs = []; - - /** Internal method that removes all dialogs */ - function removeAllDialogs(args) { - for (var i = 0; i < dialogs.length; i++) { - var dialog = dialogs[i]; - - //very special flag which means that global events cannot close this dialog - currently only used on the login - // dialog since it's special and cannot be closed without logging in. - if (!dialog.manualClose) { - dialog.close(args); - } - - } - } - - /** Internal method that closes the dialog properly and cleans up resources */ - function closeDialog(dialog) { - - if (dialog.element) { - dialog.element.modal('hide'); - - //this is not entirely enough since the damn webforms scriploader still complains - if (dialog.iframe) { - dialog.element.find("iframe").attr("src", "about:blank"); - } - - dialog.scope.$destroy(); - - //we need to do more than just remove the element, this will not destroy the - // scope in angular 1.1x, in angular 1.2x this is taken care of but if we dont - // take care of this ourselves we have memory leaks. - dialog.element.remove(); - - //remove 'this' dialog from the dialogs array - dialogs = _.reject(dialogs, function (i) { return i === dialog; }); - } - } - - /** Internal method that handles opening all dialogs */ - function openDialog(options) { - var defaults = { - container: $("body"), - animation: "fade", - modalClass: "umb-modal", - width: "100%", - inline: false, - iframe: false, - show: true, - template: "views/common/notfound.html", - callback: undefined, - closeCallback: undefined, - element: undefined, - // It will set this value as a property on the dialog controller's scope as dialogData, - // used to pass in custom data to the dialog controller's $scope. Though this is near identical to - // the dialogOptions property that is also set the the dialog controller's $scope object. - // So there's basically 2 ways of doing the same thing which we're now stuck with and in fact - // dialogData has another specially attached property called .selection which gets used. - dialogData: undefined - }; - - var dialog = angular.extend(defaults, options); - - //NOTE: People should NOT pass in a scope object that is legacy functoinality and causes problems. We will ALWAYS - // destroy the scope when the dialog is closed regardless if it is in use elsewhere which is why it shouldn't be done. - var scope = options.scope || $rootScope.$new(); - - //Modal dom obj and set id to old-dialog-service - used until we get all dialogs moved the the new overlay directive - dialog.element = $('
    '); - var id = "old-dialog-service"; - - if (options.inline) { - dialog.animation = ""; - } - else { - dialog.element.addClass("modal"); - dialog.element.addClass("hide"); - } - - //set the id and add classes - dialog.element - .attr('id', id) - .addClass(dialog.animation) - .addClass(dialog.modalClass); - - //push the modal into the global modal collection - //we halt the .push because a link click will trigger a closeAll right away - $timeout(function () { - dialogs.push(dialog); - }, 500); - - - dialog.close = function (data) { - if (dialog.closeCallback) { - dialog.closeCallback(data); - } - - closeDialog(dialog); - }; - - //if iframe is enabled, inject that instead of a template - if (dialog.iframe) { - var html = $(""); - dialog.element.html(html); - - //append to body or whatever element is passed in as options.containerElement - dialog.container.append(dialog.element); - - // Compile modal content - $timeout(function () { - $compile(dialog.element)(dialog.scope); - }); - - dialog.element.css("width", dialog.width); - - //Autoshow - if (dialog.show) { - dialog.element.modal('show'); - } - - dialog.scope = scope; - return dialog; - } - else { - - //We need to load the template with an httpget and once it's loaded we'll compile and assign the result to the container - // object. However since the result could be a promise or just data we need to use a $q.when. We still need to return the - // $modal object so we'll actually return the modal object synchronously without waiting for the promise. Otherwise this openDialog - // method will always need to return a promise which gets nasty because of promises in promises plus the result just needs a reference - // to the $modal object which will not change (only it's contents will change). - $q.when($templateCache.get(dialog.template) || $http.get(dialog.template, { cache: true }).then(function (res) { return res.data; })) - .then(function onSuccess(template) { - - // Build modal object - dialog.element.html(template); - - //append to body or other container element - dialog.container.append(dialog.element); - - // Compile modal content - $timeout(function () { - $compile(dialog.element)(scope); - }); - - scope.dialogOptions = dialog; - - //Scope to handle data from the modal form - scope.dialogData = dialog.dialogData ? dialog.dialogData : {}; - scope.dialogData.selection = []; - - // Provide scope display functions - //this passes the modal to the current scope - scope.$modal = function (name) { - dialog.element.modal(name); - }; - - scope.swipeHide = function (e) { - - if (appState.getGlobalState("touchDevice")) { - var selection = window.getSelection(); - if (selection.type !== "Range") { - scope.hide(); - } - } - }; - - //NOTE: Same as 'close' without the callbacks - scope.hide = function () { - closeDialog(dialog); - }; - - //basic events for submitting and closing - scope.submit = function (data) { - if (dialog.callback) { - dialog.callback(data); - } - - closeDialog(dialog); - }; - - scope.close = function (data) { - dialog.close(data); - }; - - //NOTE: This can ONLY ever be used to show the dialog if dialog.show is false (autoshow). - // You CANNOT call show() after you call hide(). hide = close, they are the same thing and once - // a dialog is closed it's resources are disposed of. - scope.show = function () { - if (dialog.manualClose === true) { - //show and configure that the keyboard events are not enabled on this modal - dialog.element.modal({ keyboard: false }); - } - else { - //just show normally - dialog.element.modal('show'); - } - - }; - - scope.select = function (item) { - var i = scope.dialogData.selection.indexOf(item); - if (i < 0) { - scope.dialogData.selection.push(item); - } else { - scope.dialogData.selection.splice(i, 1); - } - }; - - //NOTE: Same as 'close' without the callbacks - scope.dismiss = scope.hide; - - // Emit modal events - angular.forEach(['show', 'shown', 'hide', 'hidden'], function (name) { - dialog.element.on(name, function (ev) { - scope.$emit('modal-' + name, ev); - }); - }); - - // Support autofocus attribute - dialog.element.on('shown', function (event) { - $('input[autofocus]', dialog.element).first().trigger('focus'); - }); - - dialog.scope = scope; - - //Autoshow - if (dialog.show) { - scope.show(); - } - - }); - - //Return the modal object outside of the promise! - return dialog; - } - } - - /** Handles the closeDialogs event */ - eventsService.on("app.closeDialogs", function (evt, args) { - removeAllDialogs(args); - }); - - return { - /** + */ + angular.module('umbraco.services').factory('dialogService', function ($rootScope, $compile, $http, $timeout, $q, $templateCache, appState, eventsService) { + var dialogs = []; + /** Internal method that removes all dialogs */ + function removeAllDialogs(args) { + for (var i = 0; i < dialogs.length; i++) { + var dialog = dialogs[i]; + //very special flag which means that global events cannot close this dialog - currently only used on the login + // dialog since it's special and cannot be closed without logging in. + if (!dialog.manualClose) { + dialog.close(args); + } + } + } + /** Internal method that closes the dialog properly and cleans up resources */ + function closeDialog(dialog) { + if (dialog.element) { + dialog.element.modal('hide'); + //this is not entirely enough since the damn webforms scriploader still complains + if (dialog.iframe) { + dialog.element.find('iframe').attr('src', 'about:blank'); + } + dialog.scope.$destroy(); + //we need to do more than just remove the element, this will not destroy the + // scope in angular 1.1x, in angular 1.2x this is taken care of but if we dont + // take care of this ourselves we have memory leaks. + dialog.element.remove(); + //remove 'this' dialog from the dialogs array + dialogs = _.reject(dialogs, function (i) { + return i === dialog; + }); + } + } + /** Internal method that handles opening all dialogs */ + function openDialog(options) { + var defaults = { + container: $('body'), + animation: 'fade', + modalClass: 'umb-modal', + width: '100%', + inline: false, + iframe: false, + show: true, + template: 'views/common/notfound.html', + callback: undefined, + closeCallback: undefined, + element: undefined, + // It will set this value as a property on the dialog controller's scope as dialogData, + // used to pass in custom data to the dialog controller's $scope. Though this is near identical to + // the dialogOptions property that is also set the the dialog controller's $scope object. + // So there's basically 2 ways of doing the same thing which we're now stuck with and in fact + // dialogData has another specially attached property called .selection which gets used. + dialogData: undefined + }; + var dialog = angular.extend(defaults, options); + //NOTE: People should NOT pass in a scope object that is legacy functoinality and causes problems. We will ALWAYS + // destroy the scope when the dialog is closed regardless if it is in use elsewhere which is why it shouldn't be done. + var scope = options.scope || $rootScope.$new(); + //Modal dom obj and set id to old-dialog-service - used until we get all dialogs moved the the new overlay directive + dialog.element = $('
    '); + var id = 'old-dialog-service'; + if (options.inline) { + dialog.animation = ''; + } else { + dialog.element.addClass('modal'); + dialog.element.addClass('hide'); + } + //set the id and add classes + dialog.element.attr('id', id).addClass(dialog.animation).addClass(dialog.modalClass); + //push the modal into the global modal collection + //we halt the .push because a link click will trigger a closeAll right away + $timeout(function () { + dialogs.push(dialog); + }, 500); + dialog.close = function (data) { + if (dialog.closeCallback) { + dialog.closeCallback(data); + } + closeDialog(dialog); + }; + //if iframe is enabled, inject that instead of a template + if (dialog.iframe) { + var html = $(''); + dialog.element.html(html); + //append to body or whatever element is passed in as options.containerElement + dialog.container.append(dialog.element); + // Compile modal content + $timeout(function () { + $compile(dialog.element)(dialog.scope); + }); + dialog.element.css('width', dialog.width); + //Autoshow + if (dialog.show) { + dialog.element.modal('show'); + } + dialog.scope = scope; + return dialog; + } else { + //We need to load the template with an httpget and once it's loaded we'll compile and assign the result to the container + // object. However since the result could be a promise or just data we need to use a $q.when. We still need to return the + // $modal object so we'll actually return the modal object synchronously without waiting for the promise. Otherwise this openDialog + // method will always need to return a promise which gets nasty because of promises in promises plus the result just needs a reference + // to the $modal object which will not change (only it's contents will change). + $q.when($templateCache.get(dialog.template) || $http.get(dialog.template, { cache: true }).then(function (res) { + return res.data; + })).then(function onSuccess(template) { + // Build modal object + dialog.element.html(template); + //append to body or other container element + dialog.container.append(dialog.element); + // Compile modal content + $timeout(function () { + $compile(dialog.element)(scope); + }); + scope.dialogOptions = dialog; + //Scope to handle data from the modal form + scope.dialogData = dialog.dialogData ? dialog.dialogData : {}; + scope.dialogData.selection = []; + // Provide scope display functions + //this passes the modal to the current scope + scope.$modal = function (name) { + dialog.element.modal(name); + }; + scope.swipeHide = function (e) { + if (appState.getGlobalState('touchDevice')) { + var selection = window.getSelection(); + if (selection.type !== 'Range') { + scope.hide(); + } + } + }; + //NOTE: Same as 'close' without the callbacks + scope.hide = function () { + closeDialog(dialog); + }; + //basic events for submitting and closing + scope.submit = function (data) { + if (dialog.callback) { + dialog.callback(data); + } + closeDialog(dialog); + }; + scope.close = function (data) { + dialog.close(data); + }; + //NOTE: This can ONLY ever be used to show the dialog if dialog.show is false (autoshow). + // You CANNOT call show() after you call hide(). hide = close, they are the same thing and once + // a dialog is closed it's resources are disposed of. + scope.show = function () { + if (dialog.manualClose === true) { + //show and configure that the keyboard events are not enabled on this modal + dialog.element.modal({ keyboard: false }); + } else { + //just show normally + dialog.element.modal('show'); + } + }; + scope.select = function (item) { + var i = scope.dialogData.selection.indexOf(item); + if (i < 0) { + scope.dialogData.selection.push(item); + } else { + scope.dialogData.selection.splice(i, 1); + } + }; + //NOTE: Same as 'close' without the callbacks + scope.dismiss = scope.hide; + // Emit modal events + angular.forEach([ + 'show', + 'shown', + 'hide', + 'hidden' + ], function (name) { + dialog.element.on(name, function (ev) { + scope.$emit('modal-' + name, ev); + }); + }); + // Support autofocus attribute + dialog.element.on('shown', function (event) { + $('input[autofocus]', dialog.element).first().trigger('focus'); + }); + dialog.scope = scope; + //Autoshow + if (dialog.show) { + scope.show(); + } + }); + //Return the modal object outside of the promise! + return dialog; + } + } + /** Handles the closeDialogs event */ + eventsService.on('app.closeDialogs', function (evt, args) { + removeAllDialogs(args); + }); + return { + /** * @ngdoc method * @name umbraco.services.dialogService#open * @methodOf umbraco.services.dialogService @@ -2154,12 +1869,11 @@ angular.module('umbraco.services') * @param {Int} options.width set a width on the modal, only needed for iframes * @param {Bool} options.inline strips the modal from any animation and wrappers, used when you want to inject a dialog into an existing container * @returns {Object} modal object - */ - open: function (options) { - return openDialog(options); - }, - - /** + */ + open: function (options) { + return openDialog(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#close * @methodOf umbraco.services.dialogService @@ -2168,14 +1882,13 @@ angular.module('umbraco.services') * Closes a specific dialog * @param {Object} dialog the dialog object to close * @param {Object} args if specified this object will be sent to any callbacks registered on the dialogs. - */ - close: function (dialog, args) { - if (dialog) { - dialog.close(args); - } - }, - - /** + */ + close: function (dialog, args) { + if (dialog) { + dialog.close(args); + } + }, + /** * @ngdoc method * @name umbraco.services.dialogService#closeAll * @methodOf umbraco.services.dialogService @@ -2183,12 +1896,11 @@ angular.module('umbraco.services') * @description * Closes all dialogs * @param {Object} args if specified this object will be sent to any callbacks registered on the dialogs. - */ - closeAll: function (args) { - removeAllDialogs(args); - }, - - /** + */ + closeAll: function (args) { + removeAllDialogs(args); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#mediaPicker * @methodOf umbraco.services.dialogService @@ -2199,15 +1911,13 @@ angular.module('umbraco.services') * @param {Boolean} options.onlyImages Only display files that have an image file-extension * @param {Function} options.callback callback function * @returns {Object} modal object - */ - mediaPicker: function (options) { - options.template = 'views/common/dialogs/mediaPicker.html'; - options.show = true; - return openDialog(options); - }, - - - /** + */ + mediaPicker: function (options) { + options.template = 'views/common/dialogs/mediaPicker.html'; + options.show = true; + return openDialog(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#contentPicker * @methodOf umbraco.services.dialogService @@ -2218,16 +1928,13 @@ angular.module('umbraco.services') * @param {Boolean} options.multiPicker should the picker return one or multiple items * @param {Function} options.callback callback function * @returns {Object} modal object - */ - contentPicker: function (options) { - - options.treeAlias = "content"; - options.section = "content"; - - return this.treePicker(options); - }, - - /** + */ + contentPicker: function (options) { + options.treeAlias = 'content'; + options.section = 'content'; + return this.treePicker(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#linkPicker * @methodOf umbraco.services.dialogService @@ -2237,14 +1944,13 @@ angular.module('umbraco.services') * @param {Object} options content picker dialog options object * @param {Function} options.callback callback function * @returns {Object} modal object - */ - linkPicker: function (options) { - options.template = 'views/common/dialogs/linkPicker.html'; - options.show = true; - return openDialog(options); - }, - - /** + */ + linkPicker: function (options) { + options.template = 'views/common/dialogs/linkPicker.html'; + options.show = true; + return openDialog(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#macroPicker * @methodOf umbraco.services.dialogService @@ -2254,15 +1960,14 @@ angular.module('umbraco.services') * @param {Object} options macropicker dialog options object * @param {Function} options.callback callback function * @returns {Object} modal object - */ - macroPicker: function (options) { - options.template = 'views/common/dialogs/insertmacro.html'; - options.show = true; - options.modalClass = "span7 umb-modal"; - return openDialog(options); - }, - - /** + */ + macroPicker: function (options) { + options.template = 'views/common/dialogs/insertmacro.html'; + options.show = true; + options.modalClass = 'span7 umb-modal'; + return openDialog(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#memberPicker * @methodOf umbraco.services.dialogService @@ -2273,16 +1978,13 @@ angular.module('umbraco.services') * @param {Boolean} options.multiPicker should the tree pick one or multiple members before returning * @param {Function} options.callback callback function * @returns {Object} modal object - */ - memberPicker: function (options) { - - options.treeAlias = "member"; - options.section = "member"; - - return this.treePicker(options); - }, - - /** + */ + memberPicker: function (options) { + options.treeAlias = 'member'; + options.section = 'member'; + return this.treePicker(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#memberGroupPicker * @methodOf umbraco.services.dialogService @@ -2293,14 +1995,13 @@ angular.module('umbraco.services') * @param {Boolean} options.multiPicker should the tree pick one or multiple members before returning * @param {Function} options.callback callback function * @returns {Object} modal object - */ - memberGroupPicker: function (options) { - options.template = 'views/common/dialogs/memberGroupPicker.html'; - options.show = true; - return openDialog(options); - }, - - /** + */ + memberGroupPicker: function (options) { + options.template = 'views/common/dialogs/memberGroupPicker.html'; + options.show = true; + return openDialog(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#iconPicker * @methodOf umbraco.services.dialogService @@ -2310,14 +2011,13 @@ angular.module('umbraco.services') * @param {Object} options iconpicker dialog options object * @param {Function} options.callback callback function * @returns {Object} modal object - */ - iconPicker: function (options) { - options.template = 'views/common/dialogs/iconPicker.html'; - options.show = true; - return openDialog(options); - }, - - /** + */ + iconPicker: function (options) { + options.template = 'views/common/dialogs/iconPicker.html'; + options.show = true; + return openDialog(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#treePicker * @methodOf umbraco.services.dialogService @@ -2330,14 +2030,13 @@ angular.module('umbraco.services') * @param {Boolean} options.multiPicker should the tree pick one or multiple items before returning * @param {Function} options.callback callback function * @returns {Object} modal object - */ - treePicker: function (options) { - options.template = 'views/common/dialogs/treePicker.html'; - options.show = true; - return openDialog(options); - }, - - /** + */ + treePicker: function (options) { + options.template = 'views/common/dialogs/treePicker.html'; + options.show = true; + return openDialog(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#propertyDialog * @methodOf umbraco.services.dialogService @@ -2349,129 +2048,104 @@ angular.module('umbraco.services') * @param {String} editor editor to use to edit a given value and return on callback * @param {Object} value value sent to the property editor * @returns {Object} modal object - */ - //TODO: Wtf does this do? I don't think anything! - propertyDialog: function (options) { - options.template = 'views/common/dialogs/property.html'; - options.show = true; - return openDialog(options); - }, - - /** + */ + //TODO: Wtf does this do? I don't think anything! + propertyDialog: function (options) { + options.template = 'views/common/dialogs/property.html'; + options.show = true; + return openDialog(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#embedDialog * @methodOf umbraco.services.dialogService * @description * Opens a dialog to an embed dialog - */ - embedDialog: function (options) { - options.template = 'views/common/dialogs/rteembed.html'; - options.show = true; - return openDialog(options); - }, - /** + */ + embedDialog: function (options) { + options.template = 'views/common/dialogs/rteembed.html'; + options.show = true; + return openDialog(options); + }, + /** * @ngdoc method * @name umbraco.services.dialogService#ysodDialog * @methodOf umbraco.services.dialogService * * @description * Opens a dialog to show a custom YSOD - */ - ysodDialog: function (ysodError) { - - var newScope = $rootScope.$new(); - newScope.error = ysodError; - return openDialog({ - modalClass: "umb-modal wide ysod", - scope: newScope, - //callback: options.callback, - template: 'views/common/dialogs/ysod.html', - show: true - }); - }, - - confirmDialog: function (ysodError) { - - options.template = 'views/common/dialogs/confirm.html'; - options.show = true; - return openDialog(options); - } - }; -}); - -(function() { - 'use strict'; - - function entityHelper() { - - function getEntityTypeFromSection(section) { - if (section === "member") { - return "Member"; - } - else if (section === "media") { - return "Media"; - } else { - return "Document"; + */ + ysodDialog: function (ysodError) { + var newScope = $rootScope.$new(); + newScope.error = ysodError; + return openDialog({ + modalClass: 'umb-modal wide ysod', + scope: newScope, + //callback: options.callback, + template: 'views/common/dialogs/ysod.html', + show: true + }); + }, + confirmDialog: function (ysodError) { + options.template = 'views/common/dialogs/confirm.html'; + options.show = true; + return openDialog(options); } - } - - //////////// - - var service = { - getEntityTypeFromSection: getEntityTypeFromSection }; - - return service; - - } - - angular.module('umbraco.services').factory('entityHelper', entityHelper); - -})(); - -/** Used to broadcast and listen for global events and allow the ability to add async listeners to the callbacks */ - -/* + }); + (function () { + 'use strict'; + function entityHelper() { + function getEntityTypeFromSection(section) { + if (section === 'member') { + return 'Member'; + } else if (section === 'media') { + return 'Media'; + } else { + return 'Document'; + } + } + //////////// + var service = { getEntityTypeFromSection: getEntityTypeFromSection }; + return service; + } + angular.module('umbraco.services').factory('entityHelper', entityHelper); + }()); + /** Used to broadcast and listen for global events and allow the ability to add async listeners to the callbacks */ + /* Core app events: app.ready app.authenticated app.notAuthenticated - app.closeDialogs -*/ - -function eventsService($q, $rootScope) { - - return { - - /** raise an event with a given name, returns an array of promises for each listener */ - emit: function (name, args) { - - //there are no listeners - if (!$rootScope.$$listeners[name]) { - return; - //return []; - } - - //send the event - $rootScope.$emit(name, args); - - - //PP: I've commented out the below, since we currently dont - // expose the eventsService as a documented api - // and think we need to figure out our usecases for this - // since the below modifies the return value of the then on() method - /* + app.closeDialogs + app.ysod + app.reInitialize + app.userRefresh +*/ + function eventsService($q, $rootScope) { + return { + /** raise an event with a given name, returns an array of promises for each listener */ + emit: function (name, args) { + //there are no listeners + if (!$rootScope.$$listeners[name]) { + return; //return []; + } + //send the event + $rootScope.$emit(name, args); //PP: I've commented out the below, since we currently dont + // expose the eventsService as a documented api + // and think we need to figure out our usecases for this + // since the below modifies the return value of the then on() method + /* //setup a deferred promise for each listener var deferred = []; for (var i = 0; i < $rootScope.$$listeners[name].length; i++) { deferred.push($q.defer()); - }*/ - - //create a new event args object to pass to the - // $emit containing methods that will allow listeners - // to return data in an async if required - /* + }*/ + //create a new event args object to pass to the + // $emit containing methods that will allow listeners + // to return data in an async if required + /* var eventArgs = { args: args, reject: function (a) { @@ -2480,34 +2154,28 @@ function eventsService($q, $rootScope) { resolve: function (a) { deferred.pop().resolve(a); } - };*/ - - - - /* + };*/ + /* //return an array of promises var promises = _.map(deferred, function(p) { return p.promise; }); - return promises;*/ - }, - - /** subscribe to a method, or use scope.$on = same thing */ - on: function(name, callback) { - return $rootScope.$on(name, callback); - }, - - /** pass in the result of 'on' to this method, or just call the method returned from 'on' to unsubscribe */ - unsubscribe: function(handle) { - if (angular.isFunction(handle)) { - handle(); - } - } - }; -} - -angular.module('umbraco.services').factory('eventsService', eventsService); -/** + return promises;*/ + }, + /** subscribe to a method, or use scope.$on = same thing */ + on: function (name, callback) { + return $rootScope.$on(name, callback); + }, + /** pass in the result of 'on' to this method, or just call the method returned from 'on' to unsubscribe */ + unsubscribe: function (handle) { + if (angular.isFunction(handle)) { + handle(); + } + } + }; + } + angular.module('umbraco.services').factory('eventsService', eventsService); + /** * @ngdoc service * @name umbraco.services.fileManager * @function @@ -2516,13 +2184,11 @@ angular.module('umbraco.services').factory('eventsService', eventsService); * Used by editors to manage any files that require uploading with the posted data, normally called by property editors * that need to attach files. * When a route changes successfully, we ensure that the collection is cleared. - */ -function fileManager() { - - var fileCollection = []; - - return { - /** + */ + function fileManager() { + var fileCollection = []; + return { + /** * @ngdoc function * @name umbraco.services.fileManager#addFiles * @methodOf umbraco.services.fileManager @@ -2531,19 +2197,21 @@ function fileManager() { * @description * Attaches files to the current manager for the current editor for a particular property, if an empty array is set * for the files collection that effectively clears the files for the specified editor. - */ - setFiles: function(propertyAlias, files) { - //this will clear the files for the current property and then add the new ones for the current property - fileCollection = _.reject(fileCollection, function (item) { - return item.alias === propertyAlias; - }); - for (var i = 0; i < files.length; i++) { - //save the file object to the files collection - fileCollection.push({ alias: propertyAlias, file: files[i] }); - } - }, - - /** + */ + setFiles: function (propertyAlias, files) { + //this will clear the files for the current property and then add the new ones for the current property + fileCollection = _.reject(fileCollection, function (item) { + return item.alias === propertyAlias; + }); + for (var i = 0; i < files.length; i++) { + //save the file object to the files collection + fileCollection.push({ + alias: propertyAlias, + file: files[i] + }); + } + }, + /** * @ngdoc function * @name umbraco.services.fileManager#getFiles * @methodOf umbraco.services.fileManager @@ -2551,12 +2219,11 @@ function fileManager() { * * @description * Returns all of the files attached to the file manager - */ - getFiles: function() { - return fileCollection; - }, - - /** + */ + getFiles: function () { + return fileCollection; + }, + /** * @ngdoc function * @name umbraco.services.fileManager#clearFiles * @methodOf umbraco.services.fileManager @@ -2564,15 +2231,14 @@ function fileManager() { * * @description * Removes all files from the manager - */ - clearFiles: function () { - fileCollection = []; - } -}; -} - -angular.module('umbraco.services').factory('fileManager', fileManager); -/** + */ + clearFiles: function () { + fileCollection = []; + } + }; + } + angular.module('umbraco.services').factory('fileManager', fileManager); + /** * @ngdoc service * @name umbraco.services.formHelper * @function @@ -2580,11 +2246,10 @@ angular.module('umbraco.services').factory('fileManager', fileManager); * @description * A utility class used to streamline how forms are developed, to ensure that validation is check and displayed consistently and to ensure that the correct events * fire when they need to. - */ -function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService, localizationService) { - return { - - /** + */ + function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService, localizationService) { + return { + /** * @ngdoc function * @name umbraco.services.formHelper#submitForm * @methodOf umbraco.services.formHelper @@ -2596,60 +2261,53 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati * This returns true if the form is valid, otherwise false if form submission cannot continue. * * @param {object} args An object containing arguments for form submission - */ - submitForm: function (args) { - - var currentForm; - - if (!args) { - throw "args cannot be null"; - } - if (!args.scope) { - throw "args.scope cannot be null"; - } - if (!args.formCtrl) { - //try to get the closest form controller - currentForm = angularHelper.getRequiredCurrentForm(args.scope); - } - else { - currentForm = args.formCtrl; - } - //if no statusPropertyName is set we'll default to formStatus. - if (!args.statusPropertyName) { - args.statusPropertyName = "formStatus"; - } - //if no statusTimeout is set, we'll default to 2500 ms - if (!args.statusTimeout) { - args.statusTimeout = 2500; - } - - //the first thing any form must do is broadcast the formSubmitting event - args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action }); - - //then check if the form is valid - if (!args.skipValidation) { - if (currentForm.$invalid) { - return false; - } - } - - //reset the server validations - serverValidationManager.reset(); - - //check if a form status should be set on the scope - if (args.statusMessage) { - args.scope[args.statusPropertyName] = args.statusMessage; - - //clear the message after the timeout - $timeout(function () { - args.scope[args.statusPropertyName] = undefined; - }, args.statusTimeout); - } - - return true; - }, - - /** + */ + submitForm: function (args) { + var currentForm; + if (!args) { + throw 'args cannot be null'; + } + if (!args.scope) { + throw 'args.scope cannot be null'; + } + if (!args.formCtrl) { + //try to get the closest form controller + currentForm = angularHelper.getRequiredCurrentForm(args.scope); + } else { + currentForm = args.formCtrl; + } + //if no statusPropertyName is set we'll default to formStatus. + if (!args.statusPropertyName) { + args.statusPropertyName = 'formStatus'; + } + //if no statusTimeout is set, we'll default to 2500 ms + if (!args.statusTimeout) { + args.statusTimeout = 2500; + } + //the first thing any form must do is broadcast the formSubmitting event + args.scope.$broadcast('formSubmitting', { + scope: args.scope, + action: args.action + }); + //then check if the form is valid + if (!args.skipValidation) { + if (currentForm.$invalid) { + return false; + } + } + //reset the server validations + serverValidationManager.reset(); + //check if a form status should be set on the scope + if (args.statusMessage) { + args.scope[args.statusPropertyName] = args.statusMessage; + //clear the message after the timeout + $timeout(function () { + args.scope[args.statusPropertyName] = undefined; + }, args.statusTimeout); + } + return true; + }, + /** * @ngdoc function * @name umbraco.services.formHelper#submitForm * @methodOf umbraco.services.formHelper @@ -2660,32 +2318,36 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati * and that the notifications are displayed if there are any. * * @param {object} args An object containing arguments for form submission - */ - resetForm: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.scope) { - throw "args.scope cannot be null"; - } - - //if no statusPropertyName is set we'll default to formStatus. - if (!args.statusPropertyName) { - args.statusPropertyName = "formStatus"; - } - //clear the status - args.scope[args.statusPropertyName] = null; - - if (angular.isArray(args.notifications)) { - for (var i = 0; i < args.notifications.length; i++) { - notificationsService.showNotification(args.notifications[i]); - } - } - - args.scope.$broadcast("formSubmitted", { scope: args.scope }); - }, - - /** + */ + resetForm: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.scope) { + throw 'args.scope cannot be null'; + } + //if no statusPropertyName is set we'll default to formStatus. + if (!args.statusPropertyName) { + args.statusPropertyName = 'formStatus'; + } + //clear the status + args.scope[args.statusPropertyName] = null; + this.showNotifications(args); + args.scope.$broadcast('formSubmitted', { scope: args.scope }); + }, + showNotifications: function (args) { + if (!args || !args.notifications) { + return false; + } + if (angular.isArray(args.notifications)) { + for (var i = 0; i < args.notifications.length; i++) { + notificationsService.showNotification(args.notifications[i]); + } + return true; + } + return false; + }, + /** * @ngdoc function * @name umbraco.services.formHelper#handleError * @methodOf umbraco.services.formHelper @@ -2696,31 +2358,24 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati * add the correct messages to the notifications. If a server error has occurred this will show a ysod. * * @param {object} err The error object returned from the http promise - */ - handleError: function (err) { - //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors. - //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something). - //Or, some strange server error - if (err.status === 400) { - //now we need to look through all the validation errors - if (err.data && (err.data.ModelState)) { - - //wire up the server validation errs - this.handleServerValidation(err.data.ModelState); - - //execute all server validation events and subscribers - serverValidationManager.executeAndClearAllSubscriptions(); - } - else { - dialogService.ysodDialog(err); - } - } - else { - dialogService.ysodDialog(err); - } - }, - - /** + */ + handleError: function (err) { + //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors. + //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something). + //Or, some strange server error + if (err.status === 400) { + //now we need to look through all the validation errors + if (err.data && err.data.ModelState) { + //wire up the server validation errs + this.handleServerValidation(err.data.ModelState); + //execute all server validation events and subscribers + serverValidationManager.executeAndClearAllSubscriptions(); + } else { + dialogService.ysodDialog(err); + } + } + }, + /** * @ngdoc function * @name umbraco.services.formHelper#handleServerValidation * @methodOf umbraco.services.formHelper @@ -2730,145 +2385,111 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati * This wires up all of the server validation model state so that valServer and valServerField directives work * * @param {object} err The error object returned from the http promise - */ - handleServerValidation: function (modelState) { - for (var e in modelState) { - - //This is where things get interesting.... - // We need to support validation for all editor types such as both the content and content type editors. - // The Content editor ModelState is quite specific with the way that Properties are validated especially considering - // that each property is a User Developer property editor. - // The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations - // system. - // So, to do this (since we need to support backwards compat), we need to hack a little bit. For Content Properties, - // which are user defined, we know that they will exist with a prefixed ModelState of "_Properties.", so if we detect - // this, then we know it's a Property. - - //the alias in model state can be in dot notation which indicates - // * the first part is the content property alias - // * the second part is the field to which the valiation msg is associated with - //There will always be at least 2 parts for properties since all model errors for properties are prefixed with "Properties" - //If it is not prefixed with "Properties" that means the error is for a field of the object directly. - - var parts = e.split("."); - - //Check if this is for content properties - specific to content/media/member editors because those are special - // user defined properties with custom controls. - if (parts.length > 1 && parts[0] === "_Properties") { - - var propertyAlias = parts[1]; - - //if it contains 2 '.' then we will wire it up to a property's field - if (parts.length > 2) { - //add an error with a reference to the field for which the validation belongs too - serverValidationManager.addPropertyError(propertyAlias, parts[2], modelState[e][0]); - } - else { - //add a generic error for the property, no reference to a specific field - serverValidationManager.addPropertyError(propertyAlias, "", modelState[e][0]); - } - - } - else { - - //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: - // Groups[0].Properties[2].Alias - serverValidationManager.addFieldError(e, modelState[e][0]); - } - - //add to notifications - notificationsService.error("Validation", modelState[e][0]); - - } - } - }; -} -angular.module('umbraco.services').factory('formHelper', formHelper); -angular.module('umbraco.services') - .factory('gridService', function ($http, $q){ - - var configPath = Umbraco.Sys.ServerVariables.umbracoUrls.gridConfig; + */ + handleServerValidation: function (modelState) { + for (var e in modelState) { + //This is where things get interesting.... + // We need to support validation for all editor types such as both the content and content type editors. + // The Content editor ModelState is quite specific with the way that Properties are validated especially considering + // that each property is a User Developer property editor. + // The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations + // system. + // So, to do this (since we need to support backwards compat), we need to hack a little bit. For Content Properties, + // which are user defined, we know that they will exist with a prefixed ModelState of "_Properties.", so if we detect + // this, then we know it's a Property. + //the alias in model state can be in dot notation which indicates + // * the first part is the content property alias + // * the second part is the field to which the valiation msg is associated with + //There will always be at least 2 parts for properties since all model errors for properties are prefixed with "Properties" + //If it is not prefixed with "Properties" that means the error is for a field of the object directly. + var parts = e.split('.'); + //Check if this is for content properties - specific to content/media/member editors because those are special + // user defined properties with custom controls. + if (parts.length > 1 && parts[0] === '_Properties') { + var propertyAlias = parts[1]; + //if it contains 2 '.' then we will wire it up to a property's field + if (parts.length > 2) { + //add an error with a reference to the field for which the validation belongs too + serverValidationManager.addPropertyError(propertyAlias, parts[2], modelState[e][0]); + } else { + //add a generic error for the property, no reference to a specific field + serverValidationManager.addPropertyError(propertyAlias, '', modelState[e][0]); + } + } else { + //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: + // Groups[0].Properties[2].Alias + serverValidationManager.addFieldError(e, modelState[e][0]); + } + //add to notifications + notificationsService.error('Validation', modelState[e][0]); + } + } + }; + } + angular.module('umbraco.services').factory('formHelper', formHelper); + angular.module('umbraco.services').factory('gridService', function ($http, $q) { + var configPath = Umbraco.Sys.ServerVariables.umbracoUrls.gridConfig; var service = { - getGridEditors: function () { - return $http.get(configPath); - } - }; - - return service; - - }); - -angular.module('umbraco.services') - .factory('helpService', function ($http, $q){ - var helpTopics = {}; - - var defaultUrl = "http://our.umbraco.org/rss/help"; - var tvUrl = "http://umbraco.tv/feeds/help"; - - function getCachedHelp(url){ - if(helpTopics[url]){ - return helpTopics[cacheKey]; - }else{ - return null; - } - } - - function setCachedHelp(url, data){ - helpTopics[url] = data; - } - - function fetchUrl(url){ - var deferred = $q.defer(); - var found = getCachedHelp(url); - - if(found){ - deferred.resolve(found); - }else{ - - var proxyUrl = "dashboard/feedproxy.aspx?url=" + url; - $http.get(proxyUrl).then(function(data){ - var feed = $(data.data); - var topics = []; - - $('item', feed).each(function (i, item) { - var topic = {}; - topic.thumbnail = $(item).find('thumbnail').attr('url'); - topic.title = $("title", item).text(); - topic.link = $("guid", item).text(); - topic.description = $("description", item).text(); - topics.push(topic); - }); - - setCachedHelp(topics); - deferred.resolve(topics); - }); - } - - return deferred.promise; - } - - - - var service = { - findHelp: function (args) { - var url = service.getUrl(defaultUrl, args); - return fetchUrl(url); - }, - - findVideos: function (args) { - var url = service.getUrl(tvUrl, args); - return fetchUrl(url); - }, - - getUrl: function(url, args){ - return url + "?" + $.param(args); - } - }; - - return service; - - }); -/** + getGridEditors: function () { + return $http.get(configPath); + } + }; + return service; + }); + angular.module('umbraco.services').factory('helpService', function ($http, $q) { + var helpTopics = {}; + var defaultUrl = 'http://our.umbraco.org/rss/help'; + var tvUrl = 'http://umbraco.tv/feeds/help'; + function getCachedHelp(url) { + if (helpTopics[url]) { + return helpTopics[cacheKey]; + } else { + return null; + } + } + function setCachedHelp(url, data) { + helpTopics[url] = data; + } + function fetchUrl(url) { + var deferred = $q.defer(); + var found = getCachedHelp(url); + if (found) { + deferred.resolve(found); + } else { + var proxyUrl = 'dashboard/feedproxy.aspx?url=' + url; + $http.get(proxyUrl).then(function (data) { + var feed = $(data.data); + var topics = []; + $('item', feed).each(function (i, item) { + var topic = {}; + topic.thumbnail = $(item).find('thumbnail').attr('url'); + topic.title = $('title', item).text(); + topic.link = $('guid', item).text(); + topic.description = $('description', item).text(); + topics.push(topic); + }); + setCachedHelp(topics); + deferred.resolve(topics); + }); + } + return deferred.promise; + } + var service = { + findHelp: function (args) { + var url = service.getUrl(defaultUrl, args); + return fetchUrl(url); + }, + findVideos: function (args) { + var url = service.getUrl(tvUrl, args); + return fetchUrl(url); + }, + getUrl: function (url, args) { + return url + '?' + $.param(args); + } + }; + return service; + }); + /** * @ngdoc service * @name umbraco.services.historyService * @@ -2896,31 +2517,23 @@ angular.module('umbraco.services') * ); * }); * - */ -angular.module('umbraco.services') -.factory('historyService', function ($rootScope, $timeout, angularHelper, eventsService) { - - var nArray = []; - - function add(item) { - - if (!item) { - return null; - } - - var listWithoutThisItem = _.reject(nArray, function(i) { - return i.link === item.link; - }); - - //put it at the top and reassign - listWithoutThisItem.splice(0, 0, item); - nArray = listWithoutThisItem; - return nArray[0]; - - } - - return { - /** + */ + angular.module('umbraco.services').factory('historyService', function ($rootScope, $timeout, angularHelper, eventsService) { + var nArray = []; + function add(item) { + if (!item) { + return null; + } + var listWithoutThisItem = _.reject(nArray, function (i) { + return i.link === item.link; + }); + //put it at the top and reassign + listWithoutThisItem.splice(0, 0, item); + nArray = listWithoutThisItem; + return nArray[0]; + } + return { + /** * @ngdoc method * @name umbraco.services.historyService#add * @methodOf umbraco.services.historyService @@ -2933,16 +2546,24 @@ angular.module('umbraco.services') * @param {String} item.link route to the editor, ex: "/content/edit/1234" * @param {String} item.name friendly name for the history listing * @returns {Object} history item object - */ - add: function (item) { - var icon = item.icon || "icon-file"; - angularHelper.safeApply($rootScope, function () { - var result = add({ name: item.name, icon: icon, link: item.link, time: new Date() }); - eventsService.emit("historyService.add", {added: result, all: nArray}); - return result; - }); - }, - /** + */ + add: function (item) { + var icon = item.icon || 'icon-file'; + angularHelper.safeApply($rootScope, function () { + var result = add({ + name: item.name, + icon: icon, + link: item.link, + time: new Date() + }); + eventsService.emit('historyService.add', { + added: result, + all: nArray + }); + return result; + }); + }, + /** * @ngdoc method * @name umbraco.services.historyService#remove * @methodOf umbraco.services.historyService @@ -2951,30 +2572,31 @@ angular.module('umbraco.services') * Removes a history item from the users history collection, given an index to remove from. * * @param {Int} index index to remove item from - */ - remove: function (index) { - angularHelper.safeApply($rootScope, function() { - var result = nArray.splice(index, 1); - eventsService.emit("historyService.remove", { removed: result, all: nArray }); - }); - }, - - /** + */ + remove: function (index) { + angularHelper.safeApply($rootScope, function () { + var result = nArray.splice(index, 1); + eventsService.emit('historyService.remove', { + removed: result, + all: nArray + }); + }); + }, + /** * @ngdoc method * @name umbraco.services.historyService#removeAll * @methodOf umbraco.services.historyService * * @description * Removes all history items from the users history collection - */ - removeAll: function () { - angularHelper.safeApply($rootScope, function() { - nArray = []; - eventsService.emit("historyService.removeAll"); - }); - }, - - /** + */ + removeAll: function () { + angularHelper.safeApply($rootScope, function () { + nArray = []; + eventsService.emit('historyService.removeAll'); + }); + }, + /** * @ngdoc method * @name umbraco.services.historyService#getCurrent * @methodOf umbraco.services.historyService @@ -2982,6872 +2604,6671 @@ angular.module('umbraco.services') * @description * Method to return the current history collection. * - */ - getCurrent: function(){ - return nArray; - } - }; -}); -/** -* @ngdoc service -* @name umbraco.services.iconHelper -* @description A helper service for dealing with icons, mostly dealing with legacy tree icons -**/ -function iconHelper($q, $timeout) { - - var converter = [ - { oldIcon: ".sprNew", newIcon: "add" }, - { oldIcon: ".sprDelete", newIcon: "remove" }, - { oldIcon: ".sprMove", newIcon: "enter" }, - { oldIcon: ".sprCopy", newIcon: "documents" }, - { oldIcon: ".sprSort", newIcon: "navigation-vertical" }, - { oldIcon: ".sprPublish", newIcon: "globe" }, - { oldIcon: ".sprRollback", newIcon: "undo" }, - { oldIcon: ".sprProtect", newIcon: "lock" }, - { oldIcon: ".sprAudit", newIcon: "time" }, - { oldIcon: ".sprNotify", newIcon: "envelope" }, - { oldIcon: ".sprDomain", newIcon: "home" }, - { oldIcon: ".sprPermission", newIcon: "lock" }, - { oldIcon: ".sprRefresh", newIcon: "refresh" }, - { oldIcon: ".sprBinEmpty", newIcon: "trash" }, - { oldIcon: ".sprExportDocumentType", newIcon: "download-alt" }, - { oldIcon: ".sprImportDocumentType", newIcon: "page-up" }, - { oldIcon: ".sprLiveEdit", newIcon: "edit" }, - { oldIcon: ".sprCreateFolder", newIcon: "add" }, - { oldIcon: ".sprPackage2", newIcon: "box" }, - { oldIcon: ".sprLogout", newIcon: "logout" }, - { oldIcon: ".sprSave", newIcon: "save" }, - { oldIcon: ".sprSendToTranslate", newIcon: "envelope-alt" }, - { oldIcon: ".sprToPublish", newIcon: "mail-forward" }, - { oldIcon: ".sprTranslate", newIcon: "comments" }, - { oldIcon: ".sprUpdate", newIcon: "save" }, - - { oldIcon: ".sprTreeSettingDomain", newIcon: "icon-home" }, - { oldIcon: ".sprTreeDoc", newIcon: "icon-document" }, - { oldIcon: ".sprTreeDoc2", newIcon: "icon-diploma-alt" }, - { oldIcon: ".sprTreeDoc3", newIcon: "icon-notepad" }, - { oldIcon: ".sprTreeDoc4", newIcon: "icon-newspaper-alt" }, - { oldIcon: ".sprTreeDoc5", newIcon: "icon-notepad-alt" }, - - { oldIcon: ".sprTreeDocPic", newIcon: "icon-picture" }, - { oldIcon: ".sprTreeFolder", newIcon: "icon-folder" }, - { oldIcon: ".sprTreeFolder_o", newIcon: "icon-folder" }, - { oldIcon: ".sprTreeMediaFile", newIcon: "icon-music" }, - { oldIcon: ".sprTreeMediaMovie", newIcon: "icon-movie" }, - { oldIcon: ".sprTreeMediaPhoto", newIcon: "icon-picture" }, - - { oldIcon: ".sprTreeMember", newIcon: "icon-user" }, - { oldIcon: ".sprTreeMemberGroup", newIcon: "icon-users" }, - { oldIcon: ".sprTreeMemberType", newIcon: "icon-users" }, - - { oldIcon: ".sprTreeNewsletter", newIcon: "icon-file-text-alt" }, - { oldIcon: ".sprTreePackage", newIcon: "icon-box" }, - { oldIcon: ".sprTreeRepository", newIcon: "icon-server-alt" }, - - { oldIcon: ".sprTreeSettingDataType", newIcon: "icon-autofill" }, - - //TODO: - /* - { oldIcon: ".sprTreeSettingAgent", newIcon: "" }, - { oldIcon: ".sprTreeSettingCss", newIcon: "" }, - { oldIcon: ".sprTreeSettingCssItem", newIcon: "" }, - - { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" }, - { oldIcon: ".sprTreeSettingDomain", newIcon: "" }, - { oldIcon: ".sprTreeSettingLanguage", newIcon: "" }, - { oldIcon: ".sprTreeSettingScript", newIcon: "" }, - { oldIcon: ".sprTreeSettingTemplate", newIcon: "" }, - { oldIcon: ".sprTreeSettingXml", newIcon: "" }, - { oldIcon: ".sprTreeStatistik", newIcon: "" }, - { oldIcon: ".sprTreeUser", newIcon: "" }, - { oldIcon: ".sprTreeUserGroup", newIcon: "" }, - { oldIcon: ".sprTreeUserType", newIcon: "" }, - */ - - { oldIcon: "folder.png", newIcon: "icon-folder" }, - { oldIcon: "mediaphoto.gif", newIcon: "icon-picture" }, - { oldIcon: "mediafile.gif", newIcon: "icon-document" }, - - { oldIcon: ".sprTreeDeveloperCacheItem", newIcon: "icon-box" }, - { oldIcon: ".sprTreeDeveloperCacheTypes", newIcon: "icon-box" }, - { oldIcon: ".sprTreeDeveloperMacro", newIcon: "icon-cogs" }, - { oldIcon: ".sprTreeDeveloperRegistry", newIcon: "icon-windows" }, - { oldIcon: ".sprTreeDeveloperPython", newIcon: "icon-linux" } - ]; - - var imageConverter = [ - {oldImage: "contour.png", newIcon: "icon-umb-contour"} - ]; - - var collectedIcons; - - return { - - /** Used by the create dialogs for content/media types to format the data so that the thumbnails are styled properly */ - formatContentTypeThumbnails: function (contentTypes) { - for (var i = 0; i < contentTypes.length; i++) { - - if (contentTypes[i].thumbnailIsClass === undefined || contentTypes[i].thumbnailIsClass) { - contentTypes[i].cssClass = this.convertFromLegacyIcon(contentTypes[i].thumbnail); - }else { - contentTypes[i].style = "background-image: url('" + contentTypes[i].thumbnailFilePath + "');height:36px; background-position:4px 0px; background-repeat: no-repeat;background-size: 35px 35px;"; - //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this - contentTypes[i].cssClass = "custom-file"; - } - } - return contentTypes; - }, - formatContentTypeIcons: function (contentTypes) { - for (var i = 0; i < contentTypes.length; i++) { - if (!contentTypes[i].icon) { - //just to be safe (e.g. when focus was on close link and hitting save) - contentTypes[i].icon = "icon-document"; // default icon - } else { - contentTypes[i].icon = this.convertFromLegacyIcon(contentTypes[i].icon); - } - - //couldnt find replacement - if(contentTypes[i].icon.indexOf(".") > 0){ - contentTypes[i].icon = "icon-document-dashed-line"; - } - } - return contentTypes; - }, - /** If the icon is file based (i.e. it has a file path) */ - isFileBasedIcon: function (icon) { - //if it doesn't start with a '.' but contains one then we'll assume it's file based - if (icon.startsWith('..') || (!icon.startsWith('.') && icon.indexOf('.') > 1)) { - return true; - } - return false; - }, - /** If the icon is legacy */ - isLegacyIcon: function (icon) { - if(!icon) { - return false; - } - - if(icon.startsWith('..')){ - return false; - } - - if (icon.startsWith('.')) { - return true; - } - return false; - }, - /** If the tree node has a legacy icon */ - isLegacyTreeNodeIcon: function(treeNode){ - if (treeNode.iconIsClass) { - return this.isLegacyIcon(treeNode.icon); - } - return false; - }, - - /** Return a list of icons, optionally filter them */ - /** It fetches them directly from the active stylesheets in the browser */ - getIcons: function(){ - var deferred = $q.defer(); - $timeout(function(){ - if(collectedIcons){ - deferred.resolve(collectedIcons); - }else{ - collectedIcons = []; - var c = ".icon-"; - - for (var i = document.styleSheets.length - 1; i >= 0; i--) { - var classes = document.styleSheets[i].rules || document.styleSheets[i].cssRules; - - if (classes !== null) { - for(var x=0;x0){ - s = s.substring(0, hasSpace); - } - var hasPseudo = s.indexOf(":"); - if(hasPseudo>0){ - s = s.substring(0, hasPseudo); - } - - if(collectedIcons.indexOf(s) < 0){ - collectedIcons.push(s); - } - } - } - } + */ + getCurrent: function () { + return nArray; + }, + /** + * @ngdoc method + * @name umbraco.services.historyService#getLastAccessedItemForSection + * @methodOf umbraco.services.historyService + * + * @description + * Method to return the item that was last accessed in the given section + * + * @param {string} sectionAlias Alias of the section to return the last accessed item for. + */ + getLastAccessedItemForSection: function (sectionAlias) { + for (var i = 0, len = nArray.length; i < len; i++) { + var item = nArray[i]; + if (item.link.indexOf(sectionAlias + '/') === 0) { + return item; } - deferred.resolve(collectedIcons); } - }, 100); - - return deferred.promise; - }, - - /** Converts the icon from legacy to a new one if an old one is detected */ - convertFromLegacyIcon: function (icon) { - if (this.isLegacyIcon(icon)) { - //its legacy so convert it if we can - var found = _.find(converter, function (item) { - return item.oldIcon.toLowerCase() === icon.toLowerCase(); - }); - return (found ? found.newIcon : icon); + return null; } - return icon; - }, - - convertFromLegacyImage: function (icon) { - var found = _.find(imageConverter, function (item) { - return item.oldImage.toLowerCase() === icon.toLowerCase(); - }); - return (found ? found.newIcon : undefined); - }, - - /** If we detect that the tree node has legacy icons that can be converted, this will convert them */ - convertFromLegacyTreeNodeIcon: function (treeNode) { - if (this.isLegacyTreeNodeIcon(treeNode)) { - return this.convertFromLegacyIcon(treeNode.icon); - } - return treeNode.icon; - } - }; -} -angular.module('umbraco.services').factory('iconHelper', iconHelper); -/** -* @ngdoc service -* @name umbraco.services.imageHelper -* @deprecated + }; + }); + /** +* @ngdoc service +* @name umbraco.services.iconHelper +* @description A helper service for dealing with icons, mostly dealing with legacy tree icons **/ -function imageHelper(umbRequestHelper, mediaHelper) { - return { - /** - * @ngdoc function - * @name umbraco.services.imageHelper#getImagePropertyValue - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - getImagePropertyValue: function (options) { - return mediaHelper.getImagePropertyValue(options); - }, - /** - * @ngdoc function - * @name umbraco.services.imageHelper#getThumbnail - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - getThumbnail: function (options) { - return mediaHelper.getThumbnail(options); - }, - - /** - * @ngdoc function - * @name umbraco.services.imageHelper#scaleToMaxSize - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - scaleToMaxSize: function (maxSize, width, height) { - return mediaHelper.scaleToMaxSize(maxSize, width, height); - }, - - /** - * @ngdoc function - * @name umbraco.services.imageHelper#getThumbnailFromPath - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - getThumbnailFromPath: function (imagePath) { - return mediaHelper.getThumbnailFromPath(imagePath); - }, - - /** - * @ngdoc function - * @name umbraco.services.imageHelper#detectIfImageByExtension - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - detectIfImageByExtension: function (imagePath) { - return mediaHelper.detectIfImageByExtension(imagePath); - } - }; -} -angular.module('umbraco.services').factory('imageHelper', imageHelper); -// This service was based on OpenJS library available in BSD License -// http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php - -function keyboardService($window, $timeout) { - - var keyboardManagerService = {}; - - var defaultOpt = { - 'type': 'keydown', - 'propagate': false, - 'inputDisabled': false, - 'target': $window.document, - 'keyCode': false - }; - - // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken - var shift_nums = { - "`": "~", - "1": "!", - "2": "@", - "3": "#", - "4": "$", - "5": "%", - "6": "^", - "7": "&", - "8": "*", - "9": "(", - "0": ")", - "-": "_", - "=": "+", - ";": ":", - "'": "\"", - ",": "<", - ".": ">", - "/": "?", - "\\": "|" - }; - - // Special Keys - and their codes - var special_keys = { - 'esc': 27, - 'escape': 27, - 'tab': 9, - 'space': 32, - 'return': 13, - 'enter': 13, - 'backspace': 8, - - 'scrolllock': 145, - 'scroll_lock': 145, - 'scroll': 145, - 'capslock': 20, - 'caps_lock': 20, - 'caps': 20, - 'numlock': 144, - 'num_lock': 144, - 'num': 144, - - 'pause': 19, - 'break': 19, - - 'insert': 45, - 'home': 36, - 'delete': 46, - 'end': 35, - - 'pageup': 33, - 'page_up': 33, - 'pu': 33, - - 'pagedown': 34, - 'page_down': 34, - 'pd': 34, - - 'left': 37, - 'up': 38, - 'right': 39, - 'down': 40, - - 'f1': 112, - 'f2': 113, - 'f3': 114, - 'f4': 115, - 'f5': 116, - 'f6': 117, - 'f7': 118, - 'f8': 119, - 'f9': 120, - 'f10': 121, - 'f11': 122, - 'f12': 123 - }; - - var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0; - - // The event handler for bound element events - function eventHandler(e) { - e = e || $window.event; - - var code, k; - - // Find out which key is pressed - if (e.keyCode) - { - code = e.keyCode; - } - else if (e.which) { - code = e.which; - } - - var character = String.fromCharCode(code).toLowerCase(); - - if (code === 188){character = ",";} // If the user presses , when the type is onkeydown - if (code === 190){character = ".";} // If the user presses , when the type is onkeydown - - var propagate = true; - - //Now we need to determine which shortcut this event is for, we'll do this by iterating over each - //registered shortcut to find the match. We use Find here so that the loop exits as soon - //as we've found the one we're looking for - _.find(_.keys(keyboardManagerService.keyboardEvent), function(key) { - - var shortcutLabel = key; - var shortcutVal = keyboardManagerService.keyboardEvent[key]; - - // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked - var kp = 0; - - // Some modifiers key - var modifiers = { - shift: { - wanted: false, - pressed: e.shiftKey ? true : false - }, - ctrl: { - wanted: false, - pressed: e.ctrlKey ? true : false - }, - alt: { - wanted: false, - pressed: e.altKey ? true : false - }, - meta: { //Meta is Mac specific - wanted: false, - pressed: e.metaKey ? true : false - } - }; - - var keys = shortcutLabel.split("+"); - var opt = shortcutVal.opt; - var callback = shortcutVal.callback; - - // Foreach keys in label (split on +) - var l = keys.length; - for (var i = 0; i < l; i++) { - - var k = keys[i]; - switch (k) { - case 'ctrl': - case 'control': - kp++; - modifiers.ctrl.wanted = true; - break; - case 'shift': - case 'alt': - case 'meta': - kp++; - modifiers[k].wanted = true; - break; - } - - if (k.length > 1) { // If it is a special key - if (special_keys[k] === code) { - kp++; - } - } - else if (opt['keyCode']) { // If a specific key is set into the config - if (opt['keyCode'] === code) { - kp++; - } - } - else { // The special keys did not match - if (character === k) { - kp++; - } - else { - if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase - character = shift_nums[character]; - if (character === k) { - kp++; - } - } - } - } - - } //for end - - if (kp === keys.length && - modifiers.ctrl.pressed === modifiers.ctrl.wanted && - modifiers.shift.pressed === modifiers.shift.wanted && - modifiers.alt.pressed === modifiers.alt.wanted && - modifiers.meta.pressed === modifiers.meta.wanted) { - - //found the right callback! - - // Disable event handler when focus input and textarea - if (opt['inputDisabled']) { - var elt; - if (e.target) { - elt = e.target; - } else if (e.srcElement) { - elt = e.srcElement; - } - - if (elt.nodeType === 3) { elt = elt.parentNode; } - if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA') { - //This exits the Find loop - return true; - } - } - - $timeout(function () { - callback(e); - }, 1); - - if (!opt['propagate']) { // Stop the event - propagate = false; - } - - //This exits the Find loop - return true; - } - - //we haven't found one so continue looking - return false; - - }); - - // Stop the event if required - if (!propagate) { - // e.cancelBubble is supported by IE - this will kill the bubbling process. - e.cancelBubble = true; - e.returnValue = false; - - // e.stopPropagation works in Firefox. - if (e.stopPropagation) { - e.stopPropagation(); - e.preventDefault(); - } - return false; - } - } - - // Store all keyboard combination shortcuts - keyboardManagerService.keyboardEvent = {}; - - // Add a new keyboard combination shortcut - keyboardManagerService.bind = function (label, callback, opt) { - - //replace ctrl key with meta key - if(isMac && label !== "ctrl+space"){ - label = label.replace("ctrl","meta"); - } - - var elt; - // Initialize opt object - opt = angular.extend({}, defaultOpt, opt); - label = label.toLowerCase(); - elt = opt.target; - if(typeof opt.target === 'string'){ - elt = document.getElementById(opt.target); - } - - //Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding - // and raising events for now reason. So here we'll check if the event is already registered for the element - var boundValues = _.values(keyboardManagerService.keyboardEvent); - var found = _.find(boundValues, function (i) { - return i.target === elt && i.event === opt['type']; - }); - - // Store shortcut - keyboardManagerService.keyboardEvent[label] = { - 'callback': callback, - 'target': elt, - 'opt': opt - }; - - if (!found) { - //Attach the function with the event - if (elt.addEventListener) { - elt.addEventListener(opt['type'], eventHandler, false); - } else if (elt.attachEvent) { - elt.attachEvent('on' + opt['type'], eventHandler); - } else { - elt['on' + opt['type']] = eventHandler; - } - } + function iconHelper($q, $timeout) { + var converter = [ + { + oldIcon: '.sprNew', + newIcon: 'add' + }, + { + oldIcon: '.sprDelete', + newIcon: 'remove' + }, + { + oldIcon: '.sprMove', + newIcon: 'enter' + }, + { + oldIcon: '.sprCopy', + newIcon: 'documents' + }, + { + oldIcon: '.sprSort', + newIcon: 'navigation-vertical' + }, + { + oldIcon: '.sprPublish', + newIcon: 'globe' + }, + { + oldIcon: '.sprRollback', + newIcon: 'undo' + }, + { + oldIcon: '.sprProtect', + newIcon: 'lock' + }, + { + oldIcon: '.sprAudit', + newIcon: 'time' + }, + { + oldIcon: '.sprNotify', + newIcon: 'envelope' + }, + { + oldIcon: '.sprDomain', + newIcon: 'home' + }, + { + oldIcon: '.sprPermission', + newIcon: 'lock' + }, + { + oldIcon: '.sprRefresh', + newIcon: 'refresh' + }, + { + oldIcon: '.sprBinEmpty', + newIcon: 'trash' + }, + { + oldIcon: '.sprExportDocumentType', + newIcon: 'download-alt' + }, + { + oldIcon: '.sprImportDocumentType', + newIcon: 'page-up' + }, + { + oldIcon: '.sprLiveEdit', + newIcon: 'edit' + }, + { + oldIcon: '.sprCreateFolder', + newIcon: 'add' + }, + { + oldIcon: '.sprPackage2', + newIcon: 'box' + }, + { + oldIcon: '.sprLogout', + newIcon: 'logout' + }, + { + oldIcon: '.sprSave', + newIcon: 'save' + }, + { + oldIcon: '.sprSendToTranslate', + newIcon: 'envelope-alt' + }, + { + oldIcon: '.sprToPublish', + newIcon: 'mail-forward' + }, + { + oldIcon: '.sprTranslate', + newIcon: 'comments' + }, + { + oldIcon: '.sprUpdate', + newIcon: 'save' + }, + { + oldIcon: '.sprTreeSettingDomain', + newIcon: 'icon-home' + }, + { + oldIcon: '.sprTreeDoc', + newIcon: 'icon-document' + }, + { + oldIcon: '.sprTreeDoc2', + newIcon: 'icon-diploma-alt' + }, + { + oldIcon: '.sprTreeDoc3', + newIcon: 'icon-notepad' + }, + { + oldIcon: '.sprTreeDoc4', + newIcon: 'icon-newspaper-alt' + }, + { + oldIcon: '.sprTreeDoc5', + newIcon: 'icon-notepad-alt' + }, + { + oldIcon: '.sprTreeDocPic', + newIcon: 'icon-picture' + }, + { + oldIcon: '.sprTreeFolder', + newIcon: 'icon-folder' + }, + { + oldIcon: '.sprTreeFolder_o', + newIcon: 'icon-folder' + }, + { + oldIcon: '.sprTreeMediaFile', + newIcon: 'icon-music' + }, + { + oldIcon: '.sprTreeMediaMovie', + newIcon: 'icon-movie' + }, + { + oldIcon: '.sprTreeMediaPhoto', + newIcon: 'icon-picture' + }, + { + oldIcon: '.sprTreeMember', + newIcon: 'icon-user' + }, + { + oldIcon: '.sprTreeMemberGroup', + newIcon: 'icon-users' + }, + { + oldIcon: '.sprTreeMemberType', + newIcon: 'icon-users' + }, + { + oldIcon: '.sprTreeNewsletter', + newIcon: 'icon-file-text-alt' + }, + { + oldIcon: '.sprTreePackage', + newIcon: 'icon-box' + }, + { + oldIcon: '.sprTreeRepository', + newIcon: 'icon-server-alt' + }, + { + oldIcon: '.sprTreeSettingDataType', + newIcon: 'icon-autofill' + }, + //TODO: + /* + { oldIcon: ".sprTreeSettingAgent", newIcon: "" }, + { oldIcon: ".sprTreeSettingCss", newIcon: "" }, + { oldIcon: ".sprTreeSettingCssItem", newIcon: "" }, - }; - // Remove the shortcut - just specify the shortcut and I will remove the binding - keyboardManagerService.unbind = function (label) { - label = label.toLowerCase(); - var binding = keyboardManagerService.keyboardEvent[label]; - delete(keyboardManagerService.keyboardEvent[label]); - - if(!binding){return;} - - var type = binding['event'], - elt = binding['target'], - callback = binding['callback']; - - if(elt.detachEvent){ - elt.detachEvent('on' + type, callback); - }else if(elt.removeEventListener){ - elt.removeEventListener(type, callback, false); - }else{ - elt['on'+type] = false; - } - }; - // - - return keyboardManagerService; -} angular.module('umbraco.services').factory('keyboardService', ['$window', '$timeout', keyboardService]); -/** - @ngdoc service - * @name umbraco.services.listViewHelper - * - * - * @description - * Service for performing operations against items in the list view UI. Used by the built-in internal listviews - * as well as custom listview. - * - * A custom listview is always used inside a wrapper listview, so there are a number of inherited values on its - * scope by default: - * - * **$scope.selection**: Array containing all items currently selected in the listview - * - * **$scope.items**: Array containing all items currently displayed in the listview - * - * **$scope.folders**: Array containing all folders in the current listview (only for media) - * - * **$scope.options**: configuration object containing information such as pagesize, permissions, order direction etc. - * - * **$scope.model.config.layouts**: array of available layouts to apply to the listview (grid, list or custom layout) - * - * ##Usage## - * To use, inject listViewHelper into custom listview controller, listviewhelper expects you - * to pass in the full collection of items in the listview in several of its methods - * this collection is inherited from the parent controller and is available on $scope.selection - * - *
    - *      angular.module("umbraco").controller("my.listVieweditor". function($scope, listViewHelper){
    - *
    - *          //current items in the listview
    - *          var items = $scope.items;
    - *
    - *          //current selection
    - *          var selection = $scope.selection;
    - *
    - *          //deselect an item , $scope.selection is inherited, item is picked from inherited $scope.items
    - *          listViewHelper.deselectItem(item, $scope.selection);
    - *
    - *          //test if all items are selected, $scope.items + $scope.selection are inherited
    - *          listViewhelper.isSelectedAll($scope.items, $scope.selection);
    - *      });
    - * 
    - */ -(function () { - 'use strict'; - - function listViewHelper(localStorageService) { - - var firstSelectedIndex = 0; - var localStorageKey = "umblistViewLayout"; - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#getLayout - * @methodOf umbraco.services.listViewHelper - * - * @description - * Method for internal use, based on the collection of layouts passed, the method selects either - * any previous layout from local storage, or picks the first allowed layout - * - * @param {Number} nodeId The id of the current node displayed in the content editor - * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts + { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" }, + { oldIcon: ".sprTreeSettingDomain", newIcon: "" }, + { oldIcon: ".sprTreeSettingLanguage", newIcon: "" }, + { oldIcon: ".sprTreeSettingScript", newIcon: "" }, + { oldIcon: ".sprTreeSettingTemplate", newIcon: "" }, + { oldIcon: ".sprTreeSettingXml", newIcon: "" }, + { oldIcon: ".sprTreeStatistik", newIcon: "" }, + { oldIcon: ".sprTreeUser", newIcon: "" }, + { oldIcon: ".sprTreeUserGroup", newIcon: "" }, + { oldIcon: ".sprTreeUserType", newIcon: "" }, */ - - function getLayout(nodeId, availableLayouts) { - - var storedLayouts = []; - - if (localStorageService.get(localStorageKey)) { - storedLayouts = localStorageService.get(localStorageKey); - } - - if (storedLayouts && storedLayouts.length > 0) { - for (var i = 0; storedLayouts.length > i; i++) { - var layout = storedLayouts[i]; - if (layout.nodeId === nodeId) { - return setLayout(nodeId, layout, availableLayouts); - } - } - - } - - return getFirstAllowedLayout(availableLayouts); - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#setLayout - * @methodOf umbraco.services.listViewHelper - * - * @description - * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage - * - * @param {Number} nodeID Id of the current node displayed in the content editor - * @param {Object} selectedLayout Layout selected as the layout to set as the current layout - * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts - */ - - function setLayout(nodeId, selectedLayout, availableLayouts) { - - var activeLayout = {}; - var layoutFound = false; - - for (var i = 0; availableLayouts.length > i; i++) { - var layout = availableLayouts[i]; - if (layout.path === selectedLayout.path) { - activeLayout = layout; - layout.active = true; - layoutFound = true; - } else { - layout.active = false; - } - } - - if (!layoutFound) { - activeLayout = getFirstAllowedLayout(availableLayouts); + { + oldIcon: 'folder.png', + newIcon: 'icon-folder' + }, + { + oldIcon: 'mediaphoto.gif', + newIcon: 'icon-picture' + }, + { + oldIcon: 'mediafile.gif', + newIcon: 'icon-document' + }, + { + oldIcon: '.sprTreeDeveloperCacheItem', + newIcon: 'icon-box' + }, + { + oldIcon: '.sprTreeDeveloperCacheTypes', + newIcon: 'icon-box' + }, + { + oldIcon: '.sprTreeDeveloperMacro', + newIcon: 'icon-cogs' + }, + { + oldIcon: '.sprTreeDeveloperRegistry', + newIcon: 'icon-windows' + }, + { + oldIcon: '.sprTreeDeveloperPython', + newIcon: 'icon-linux' } - - saveLayoutInLocalStorage(nodeId, activeLayout); - - return activeLayout; - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#saveLayoutInLocalStorage - * @methodOf umbraco.services.listViewHelper - * - * @description - * Stores a given layout as the current default selection in local storage - * - * @param {Number} nodeId Id of the current node displayed in the content editor - * @param {Object} selectedLayout Layout selected as the layout to set as the current layout - */ - - function saveLayoutInLocalStorage(nodeId, selectedLayout) { - var layoutFound = false; - var storedLayouts = []; - - if (localStorageService.get(localStorageKey)) { - storedLayouts = localStorageService.get(localStorageKey); - } - - if (storedLayouts.length > 0) { - for (var i = 0; storedLayouts.length > i; i++) { - var layout = storedLayouts[i]; - if (layout.nodeId === nodeId) { - layout.path = selectedLayout.path; - layoutFound = true; + ]; + var imageConverter = [{ + oldImage: 'contour.png', + newIcon: 'icon-umb-contour' + }]; + var collectedIcons; + return { + /** Used by the create dialogs for content/media types to format the data so that the thumbnails are styled properly */ + formatContentTypeThumbnails: function (contentTypes) { + for (var i = 0; i < contentTypes.length; i++) { + if (contentTypes[i].thumbnailIsClass === undefined || contentTypes[i].thumbnailIsClass) { + contentTypes[i].cssClass = this.convertFromLegacyIcon(contentTypes[i].thumbnail); + } else { + contentTypes[i].style = 'background-image: url(\'' + contentTypes[i].thumbnailFilePath + '\');height:36px; background-position:4px 0px; background-repeat: no-repeat;background-size: 35px 35px;'; + //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this + contentTypes[i].cssClass = 'custom-file'; } } - } - - if (!layoutFound) { - var storageObject = { - "nodeId": nodeId, - "path": selectedLayout.path - }; - storedLayouts.push(storageObject); - } - - localStorageService.set(localStorageKey, storedLayouts); - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#getFirstAllowedLayout - * @methodOf umbraco.services.listViewHelper - * - * @description - * Returns currently selected layout, or alternatively the first layout in the available layouts collection - * - * @param {Array} layouts Array of all allowed layouts, available from $scope.model.config.layouts - */ - - function getFirstAllowedLayout(layouts) { - - var firstAllowedLayout = {}; - - for (var i = 0; layouts.length > i; i++) { - var layout = layouts[i]; - if (layout.selected === true) { - firstAllowedLayout = layout; - break; - } - } - - return firstAllowedLayout; - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#selectHandler - * @methodOf umbraco.services.listViewHelper - * - * @description - * Helper method for working with item selection via a checkbox, internally it uses selectItem and deselectItem. - * Working with this method, requires its triggered via a checkbox which can then pass in its triggered $event - * When the checkbox is clicked, this method will toggle selection of the associated item so it matches the state of the checkbox - * - * @param {Object} selectedItem Item being selected or deselected by the checkbox - * @param {Number} selectedIndex Index of item being selected/deselected, usually passed as $index - * @param {Array} items All items in the current listview, available as $scope.items - * @param {Array} selection All selected items in the current listview, available as $scope.selection - * @param {Event} $event Event triggered by the checkbox being checked to select / deselect an item - */ - - function selectHandler(selectedItem, selectedIndex, items, selection, $event) { - - var start = 0; - var end = 0; - var item = null; - - if ($event.shiftKey === true) { - - if (selectedIndex > firstSelectedIndex) { - - start = firstSelectedIndex; - end = selectedIndex; - - for (; end >= start; start++) { - item = items[start]; - selectItem(item, selection); - } - - } else { - - start = firstSelectedIndex; - end = selectedIndex; - - for (; end <= start; start--) { - item = items[start]; - selectItem(item, selection); + return contentTypes; + }, + formatContentTypeIcons: function (contentTypes) { + for (var i = 0; i < contentTypes.length; i++) { + if (!contentTypes[i].icon) { + //just to be safe (e.g. when focus was on close link and hitting save) + contentTypes[i].icon = 'icon-document'; // default icon + } else { + contentTypes[i].icon = this.convertFromLegacyIcon(contentTypes[i].icon); + } + //couldnt find replacement + if (contentTypes[i].icon.indexOf('.') > 0) { + contentTypes[i].icon = 'icon-document-dashed-line'; } - } - - } else { - - if (selectedItem.selected) { - deselectItem(selectedItem, selection); - } else { - selectItem(selectedItem, selection); + return contentTypes; + }, + /** If the icon is file based (i.e. it has a file path) */ + isFileBasedIcon: function (icon) { + //if it doesn't start with a '.' but contains one then we'll assume it's file based + if (icon.startsWith('..') || !icon.startsWith('.') && icon.indexOf('.') > 1) { + return true; } - - firstSelectedIndex = selectedIndex; - - } - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#selectItem - * @methodOf umbraco.services.listViewHelper - * - * @description - * Selects a given item to the listview selection array, requires you pass in the inherited $scope.selection collection - * - * @param {Object} item Item to select - * @param {Array} selection Listview selection, available as $scope.selection - */ - - function selectItem(item, selection) { - var isSelected = false; - for (var i = 0; selection.length > i; i++) { - var selectedItem = selection[i]; - if (item.id === selectedItem.id) { - isSelected = true; + return false; + }, + /** If the icon is legacy */ + isLegacyIcon: function (icon) { + if (!icon) { + return false; } - } - if (!isSelected) { - selection.push({ id: item.id }); - item.selected = true; - } - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#deselectItem - * @methodOf umbraco.services.listViewHelper - * - * @description - * Deselects a given item from the listviews selection array, requires you pass in the inherited $scope.selection collection - * - * @param {Object} item Item to deselect - * @param {Array} selection Listview selection, available as $scope.selection - */ - - function deselectItem(item, selection) { - for (var i = 0; selection.length > i; i++) { - var selectedItem = selection[i]; - if (item.id === selectedItem.id) { - selection.splice(i, 1); - item.selected = false; + if (icon.startsWith('..')) { + return false; } - } - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#clearSelection - * @methodOf umbraco.services.listViewHelper - * - * @description - * Removes a given number of items and folders from the listviews selection array - * Folders can only be passed in if the listview is used in the media section which has a concept of folders. - * - * @param {Array} items Items to remove, can be null - * @param {Array} folders Folders to remove, can be null - * @param {Array} selection Listview selection, available as $scope.selection - */ - - function clearSelection(items, folders, selection) { - - var i = 0; - - selection.length = 0; - - if (angular.isArray(items)) { - for (i = 0; items.length > i; i++) { - var item = items[i]; - item.selected = false; + if (icon.startsWith('.')) { + return true; } - } - - if(angular.isArray(folders)) { - for (i = 0; folders.length > i; i++) { - var folder = folders[i]; - folder.selected = false; + return false; + }, + /** If the tree node has a legacy icon */ + isLegacyTreeNodeIcon: function (treeNode) { + if (treeNode.iconIsClass) { + return this.isLegacyIcon(treeNode.icon); } - } - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#selectAllItems - * @methodOf umbraco.services.listViewHelper - * - * @description - * Helper method for toggling the select state on all items in the active listview - * Can only be used from a checkbox as a checkbox $event is required to pass in. - * - * @param {Array} items Items to toggle selection on, should be $scope.items - * @param {Array} selection Listview selection, available as $scope.selection - * @param {$event} $event Event passed from the checkbox being toggled - */ - - function selectAllItems(items, selection, $event) { - - var checkbox = $event.target; - var clearSelection = false; - - if (!angular.isArray(items)) { - return; - } - - selection.length = 0; - - for (var i = 0; i < items.length; i++) { - - var item = items[i]; - - if (checkbox.checked) { - selection.push({ id: item.id }); - } else { - clearSelection = true; + return false; + }, + /** Return a list of icons, optionally filter them */ + /** It fetches them directly from the active stylesheets in the browser */ + getIcons: function () { + var deferred = $q.defer(); + $timeout(function () { + if (collectedIcons) { + deferred.resolve(collectedIcons); + } else { + collectedIcons = []; + var c = '.icon-'; + for (var i = document.styleSheets.length - 1; i >= 0; i--) { + var classes = document.styleSheets[i].rules || document.styleSheets[i].cssRules; + if (classes !== null) { + for (var x = 0; x < classes.length; x++) { + var cur = classes[x]; + if (cur.selectorText && cur.selectorText.indexOf(c) === 0) { + var s = cur.selectorText.substring(1); + var hasSpace = s.indexOf(' '); + if (hasSpace > 0) { + s = s.substring(0, hasSpace); + } + var hasPseudo = s.indexOf(':'); + if (hasPseudo > 0) { + s = s.substring(0, hasPseudo); + } + if (collectedIcons.indexOf(s) < 0) { + collectedIcons.push(s); + } + } + } + } + } + deferred.resolve(collectedIcons); + } + }, 100); + return deferred.promise; + }, + /** Converts the icon from legacy to a new one if an old one is detected */ + convertFromLegacyIcon: function (icon) { + if (this.isLegacyIcon(icon)) { + //its legacy so convert it if we can + var found = _.find(converter, function (item) { + return item.oldIcon.toLowerCase() === icon.toLowerCase(); + }); + return found ? found.newIcon : icon; } - - item.selected = checkbox.checked; - - } - - if (clearSelection) { - selection.length = 0; - } - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#isSelectedAll - * @methodOf umbraco.services.listViewHelper - * - * @description - * Method to determine if all items on the current page in the list has been selected - * Given the current items in the view, and the current selection, it will return true/false - * - * @param {Array} items Items to test if all are selected, should be $scope.items - * @param {Array} selection Listview selection, available as $scope.selection - * @returns {Boolean} boolean indicate if all items in the listview have been selected - */ - - function isSelectedAll(items, selection) { - - var numberOfSelectedItem = 0; - - for (var itemIndex = 0; items.length > itemIndex; itemIndex++) { - var item = items[itemIndex]; - - for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) { - var selectedItem = selection[selectedIndex]; - - if (item.id === selectedItem.id) { - numberOfSelectedItem++; - } - } - - } - - if (numberOfSelectedItem === items.length) { - return true; - } - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#setSortingDirection - * @methodOf umbraco.services.listViewHelper - * - * @description - * *Internal* method for changing sort order icon - * @param {String} col Column alias to order after - * @param {String} direction Order direction `asc` or `desc` - * @param {Object} options object passed from the parent listview available as $scope.options - */ - - function setSortingDirection(col, direction, options) { - return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction; - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#setSorting - * @methodOf umbraco.services.listViewHelper - * - * @description - * Method for setting the field on which the listview will order its items after. - * - * @param {String} field Field alias to order after - * @param {Boolean} allow Determines if the user is allowed to set this field, normally true - * @param {Object} options Options object passed from the parent listview available as $scope.options - */ - - function setSorting(field, allow, options) { - if (allow) { - if (options.orderBy === field && options.orderDirection === 'asc') { - options.orderDirection = "desc"; - } else { - options.orderDirection = "asc"; + return icon; + }, + convertFromLegacyImage: function (icon) { + var found = _.find(imageConverter, function (item) { + return item.oldImage.toLowerCase() === icon.toLowerCase(); + }); + return found ? found.newIcon : undefined; + }, + /** If we detect that the tree node has legacy icons that can be converted, this will convert them */ + convertFromLegacyTreeNodeIcon: function (treeNode) { + if (this.isLegacyTreeNodeIcon(treeNode)) { + return this.convertFromLegacyIcon(treeNode.icon); } - options.orderBy = field; + return treeNode.icon; } - } - - //This takes in a dictionary of Ids with Permissions and determines - // the intersect of all permissions to return an object representing the - // listview button permissions - function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) { - - if (currentIdsWithPermissions == null) { - currentIdsWithPermissions = {}; - } - - //merge the newly retrieved permissions to the main dictionary - _.each(unmergedPermissions, function (value, key, list) { - currentIdsWithPermissions[key] = value; - }); - - //get the intersect permissions - var arr = []; - _.each(currentIdsWithPermissions, function (value, key, list) { - arr.push(value); - }); - - //we need to use 'apply' to call intersection with an array of arrays, - //see: http://stackoverflow.com/a/16229480/694494 - var intersectPermissions = _.intersection.apply(_, arr); - - return { - canCopy: _.contains(intersectPermissions, 'O'), //Magic Char = O - canCreate: _.contains(intersectPermissions, 'C'), //Magic Char = C - canDelete: _.contains(intersectPermissions, 'D'), //Magic Char = D - canMove: _.contains(intersectPermissions, 'M'), //Magic Char = M - canPublish: _.contains(intersectPermissions, 'U'), //Magic Char = U - canUnpublish: _.contains(intersectPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) - }; - } - - var service = { - - getLayout: getLayout, - getFirstAllowedLayout: getFirstAllowedLayout, - setLayout: setLayout, - saveLayoutInLocalStorage: saveLayoutInLocalStorage, - selectHandler: selectHandler, - selectItem: selectItem, - deselectItem: deselectItem, - clearSelection: clearSelection, - selectAllItems: selectAllItems, - isSelectedAll: isSelectedAll, - setSortingDirection: setSortingDirection, - setSorting: setSorting, - getButtonPermissions: getButtonPermissions - }; - - return service; - } - - - angular.module('umbraco.services').factory('listViewHelper', listViewHelper); - - -})(); - -/** - @ngdoc service - * @name umbraco.services.listViewPrevalueHelper - * - * - * @description - * Service for accessing the prevalues of a list view being edited in the inline list view editor in the doctype editor - */ -(function () { - 'use strict'; - - function listViewPrevalueHelper() { - - var prevalues = []; - - /** - * @ngdoc method - * @name umbraco.services.listViewPrevalueHelper#getPrevalues - * @methodOf umbraco.services.listViewPrevalueHelper - * - * @description - * Set the collection of prevalues - */ - - function getPrevalues() { - return prevalues; - } - - /** - * @ngdoc method - * @name umbraco.services.listViewPrevalueHelper#setPrevalues - * @methodOf umbraco.services.listViewPrevalueHelper - * - * @description - * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage - * - * @param {Array} values Array of prevalues - */ - - function setPrevalues(values) { - prevalues = values; - } - - - - var service = { - - getPrevalues: getPrevalues, - setPrevalues: setPrevalues - - }; - - return service; - - } - - - angular.module('umbraco.services').factory('listViewPrevalueHelper', listViewPrevalueHelper); - - -})(); - -/** - * @ngdoc service - * @name umbraco.services.localizationService - * - * @requires $http - * @requires $q - * @requires $window - * @requires $filter - * - * @description - * Application-wide service for handling localization - * - * ##usage - * To use, simply inject the localizationService into any controller that needs it, and make - * sure the umbraco.services module is accesible - which it should be by default. - * - *
    - *    localizationService.localize("area_key").then(function(value){
    - *        element.html(value);
    - *    });
    - * 
    - */ - -angular.module('umbraco.services') -.factory('localizationService', function ($http, $q, eventsService, $window, $filter, userService) { - - //TODO: This should be injected as server vars - var url = "LocalizedText"; - var resourceFileLoadStatus = "none"; - var resourceLoadingPromise = []; - - function _lookup(value, tokens, dictionary) { - - //strip the key identifier if its there - if (value && value[0] === "@") { - value = value.substring(1); - } - - //if no area specified, add general_ - if (value && value.indexOf("_") < 0) { - value = "general_" + value; - } - - var entry = dictionary[value]; - if (entry) { - if (tokens) { - for (var i = 0; i < tokens.length; i++) { - entry = entry.replace("%" + i + "%", tokens[i]); - } - } - return entry; - } - return "[" + value + "]"; - } - - var service = { - // array to hold the localized resource string entries - dictionary: [], - - // loads the language resource file from the server - initLocalizedResources: function () { - var deferred = $q.defer(); - - if (resourceFileLoadStatus === "loaded") { - deferred.resolve(service.dictionary); - return deferred.promise; - } - - //if the resource is already loading, we don't want to force it to load another one in tandem, we'd rather - // wait for that initial http promise to finish and then return this one with the dictionary loaded - if (resourceFileLoadStatus === "loading") { - //add to the list of promises waiting - resourceLoadingPromise.push(deferred); - - //exit now it's already loading - return deferred.promise; - } - - resourceFileLoadStatus = "loading"; - - // build the url to retrieve the localized resource file - $http({ method: "GET", url: url, cache: false }) - .then(function (response) { - resourceFileLoadStatus = "loaded"; - service.dictionary = response.data; - - eventsService.emit("localizationService.updated", response.data); - - deferred.resolve(response.data); - //ensure all other queued promises are resolved - for (var p in resourceLoadingPromise) { - resourceLoadingPromise[p].resolve(response.data); - } - }, function (err) { - deferred.reject("Something broke"); - //ensure all other queued promises are resolved - for (var p in resourceLoadingPromise) { - resourceLoadingPromise[p].reject("Something broke"); - } - }); - return deferred.promise; - }, - - /** - * @ngdoc method - * @name umbraco.services.localizationService#tokenize - * @methodOf umbraco.services.localizationService - * - * @description - * Helper to tokenize and compile a localization string - * @param {String} value the value to tokenize - * @param {Object} scope the $scope object - * @returns {String} tokenized resource string - */ - tokenize: function (value, scope) { - if (value) { - var localizer = value.split(':'); - var retval = { tokens: undefined, key: localizer[0].substring(0) }; - if (localizer.length > 1) { - retval.tokens = localizer[1].split(','); - for (var x = 0; x < retval.tokens.length; x++) { - retval.tokens[x] = scope.$eval(retval.tokens[x]); - } - } - - return retval; - } - return value; - }, - - /** - * @ngdoc method - * @name umbraco.services.localizationService#localize - * @methodOf umbraco.services.localizationService - * - * @description - * Checks the dictionary for a localized resource string - * @param {String} value the area/key to localize in the format of 'section_key' - * alternatively if no section is set such as 'key' then we assume the key is to be looked in - * the 'general' section - * - * @param {Array} tokens if specified this array will be sent as parameter values - * This replaces %0% and %1% etc in the dictionary key value with the passed in strings - * - * @returns {String} localized resource string - */ - localize: function (value, tokens) { - return service.initLocalizedResources().then(function (dic) { - var val = _lookup(value, tokens, dic); - return val; - }); - }, - - /** - * @ngdoc method - * @name umbraco.services.localizationService#localizeMany - * @methodOf umbraco.services.localizationService - * - * @description - * Checks the dictionary for multipe localized resource strings at once, preventing the need for nested promises - * with localizationService.localize - * - * ##Usage - *
    -         * localizationService.localizeMany(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){
    -         *      var header = data[0];
    -         *      var message = data[1];
    -         *      notificationService.error(header, message);
    -         * });
    -         * 
    - * - * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key' - * alternatively if no section is set such as 'key' then we assume the key is to be looked in - * the 'general' section - * - * @returns {Array} An array of localized resource string in the same order - */ - localizeMany: function(keys) { - if(keys){ - - //The LocalizationService.localize promises we want to resolve - var promises = []; - - for(var i = 0; i < keys.length; i++){ - promises.push(service.localize(keys[i], undefined)); - } - - return $q.all(promises).then(function(localizedValues){ - return localizedValues; - }); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.localizationService#concat - * @methodOf umbraco.services.localizationService + angular.module('umbraco.services').factory('iconHelper', iconHelper); + /** +* @ngdoc service +* @name umbraco.services.imageHelper +* @deprecated +**/ + function imageHelper(umbRequestHelper, mediaHelper) { + return { + /** + * @ngdoc function + * @name umbraco.services.imageHelper#getImagePropertyValue + * @methodOf umbraco.services.imageHelper + * @function * - * @description - * Checks the dictionary for multipe localized resource strings at once & concats them to a single string - * Which was not possible with localizationSerivce.localize() due to returning a promise - * - * ##Usage - *
    -         * localizationService.concat(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){
    -         *      var combinedText = data;
    -         * });
    -         * 
    - * - * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key' - * alternatively if no section is set such as 'key' then we assume the key is to be looked in - * the 'general' section - * - * @returns {String} An concatenated string of localized resource string passed into the function in the same order - */ - concat: function(keys) { - if(keys){ - - //The LocalizationService.localize promises we want to resolve - var promises = []; - - for(var i = 0; i < keys.length; i++){ - promises.push(service.localize(keys[i], undefined)); - } - - return $q.all(promises).then(function(localizedValues){ - - //Build a concat string by looping over the array of resolved promises/translations - var returnValue = ""; - - for(var i = 0; i < localizedValues.length; i++){ - returnValue += localizedValues[i]; - } - - return returnValue; - }); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.localizationService#format - * @methodOf umbraco.services.localizationService + * @deprecated + */ + getImagePropertyValue: function (options) { + return mediaHelper.getImagePropertyValue(options); + }, + /** + * @ngdoc function + * @name umbraco.services.imageHelper#getThumbnail + * @methodOf umbraco.services.imageHelper + * @function * - * @description - * Checks the dictionary for multipe localized resource strings at once & formats a tokenized message - * Which was not possible with localizationSerivce.localize() due to returning a promise - * - * ##Usage - *
    -         * localizationService.format(["template_insert", "template_insertSections"], "%0% %1%").then(function(data){
    -         *      //Will return 'Insert Sections'
    -         *      var formattedResult = data;
    -         * });
    -         * 
    - * - * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key' - * alternatively if no section is set such as 'key' then we assume the key is to be looked in - * the 'general' section - * - * @param {String} message is the string you wish to replace containing tokens in the format of %0% and %1% - * with the localized resource strings - * - * @returns {String} An concatenated string of localized resource string passed into the function in the same order - */ - format: function(keys, message){ - if(keys){ - - //The LocalizationService.localize promises we want to resolve - var promises = []; - - for(var i = 0; i < keys.length; i++){ - promises.push(service.localize(keys[i], undefined)); - } - - return $q.all(promises).then(function(localizedValues){ - - //Replace {0} and {1} etc in message with the localized values - for(var i = 0; i < localizedValues.length; i++){ - var token = "%" + i + "%"; - var regex = new RegExp(token, "g"); - - message = message.replace(regex, localizedValues[i]); - } - - return message; - }); - } - } - - }; - - //This happens after login / auth and assets loading - eventsService.on("app.authenticated", function () { - resourceFileLoadStatus = "none"; - resourceLoadingPromise = []; - }); - - // return the local instance when called - return service; -}); - -/** - * @ngdoc service - * @name umbraco.services.macroService - * - * - * @description - * A service to return macro information such as generating syntax to insert a macro into an editor - */ -function macroService() { - - return { - - /** parses the special macro syntax like and returns an object with the macro alias and it's parameters */ - parseMacroSyntax: function (syntax) { - - //This regex will match an alias of anything except characters that are quotes or new lines (for legacy reasons, when new macros are created - // their aliases are cleaned an invalid chars are stripped) - var expression = /(<\?UMBRACO_MACRO (?:.+?)?macroAlias=["']([^\"\'\n\r]+?)["'][\s\S]+?)(\/>|>.*?<\/\?UMBRACO_MACRO>)/i; - var match = expression.exec(syntax); - if (!match || match.length < 3) { - return null; - } - var alias = match[2]; - - //this will leave us with just the parameters - var paramsChunk = match[1].trim().replace(new RegExp("UMBRACO_MACRO macroAlias=[\"']" + alias + "[\"']"), "").trim(); - - var paramExpression = /(\w+?)=['\"]([\s\S]*?)['\"]/g; - - var paramMatch; - var returnVal = { - macroAlias: alias, - macroParamsDictionary: {} - }; - while (paramMatch = paramExpression.exec(paramsChunk)) { - returnVal.macroParamsDictionary[paramMatch[1]] = paramMatch[2]; - } - return returnVal; - }, - - /** + * @deprecated + */ + getThumbnail: function (options) { + return mediaHelper.getThumbnail(options); + }, + /** * @ngdoc function - * @name umbraco.services.macroService#generateWebFormsSyntax - * @methodOf umbraco.services.macroService + * @name umbraco.services.imageHelper#scaleToMaxSize + * @methodOf umbraco.services.imageHelper * @function * - * @description - * generates the syntax for inserting a macro into a rich text editor - this is the very old umbraco style syntax - * - * @param {object} args an object containing the macro alias and it's parameter values - */ - generateMacroSyntax: function (args) { - - // - - var macroString = '"; - - return macroString; - }, - - /** + * @deprecated + */ + scaleToMaxSize: function (maxSize, width, height) { + return mediaHelper.scaleToMaxSize(maxSize, width, height); + }, + /** * @ngdoc function - * @name umbraco.services.macroService#generateWebFormsSyntax - * @methodOf umbraco.services.macroService + * @name umbraco.services.imageHelper#getThumbnailFromPath + * @methodOf umbraco.services.imageHelper * @function * - * @description - * generates the syntax for inserting a macro into a webforms templates - * - * @param {object} args an object containing the macro alias and it's parameter values - */ - generateWebFormsSyntax: function(args) { - - var macroString = '"; - - return macroString; - }, - - /** + * @deprecated + */ + getThumbnailFromPath: function (imagePath) { + return mediaHelper.getThumbnailFromPath(imagePath); + }, + /** * @ngdoc function - * @name umbraco.services.macroService#generateMvcSyntax - * @methodOf umbraco.services.macroService + * @name umbraco.services.imageHelper#detectIfImageByExtension + * @methodOf umbraco.services.imageHelper * @function * - * @description - * generates the syntax for inserting a macro into an mvc template - * - * @param {object} args an object containing the macro alias and it's parameter values - */ - generateMvcSyntax: function (args) { - - var macroString = "@Umbraco.RenderMacro(\"" + args.macroAlias + "\""; - - var hasParams = false; - var paramString; - if (args.macroParamsDictionary) { - - paramString = ", new {"; - - _.each(args.macroParamsDictionary, function(val, key) { - - hasParams = true; - - var keyVal = key + "=\"" + (val ? val : "") + "\", "; - - paramString += keyVal; - }); - - //remove the last , - paramString = paramString.trimEnd(", "); - - paramString += "}"; - } - if (hasParams) { - macroString += paramString; - } - - macroString += ")"; - return macroString; - }, - - collectValueData: function(macro, macroParams, renderingEngine) { - - var paramDictionary = {}; - var macroAlias = macro.alias; - var syntax; - - _.each(macroParams, function (item) { - - var val = item.value; - - if (item.value !== null && item.value !== undefined && !_.isString(item.value)) { - try { - val = angular.toJson(val); - } - catch (e) { - // not json - } - } - - //each value needs to be xml escaped!! since the value get's stored as an xml attribute - paramDictionary[item.alias] = _.escape(val); - - }); - - //get the syntax based on the rendering engine - if (renderingEngine && renderingEngine === "WebForms") { - syntax = this.generateWebFormsSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); - } - else if (renderingEngine && renderingEngine === "Mvc") { - syntax = this.generateMvcSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); - } - else { - syntax = this.generateMacroSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); - } - - var macroObject = { - "macroParamsDictionary": paramDictionary, - "macroAlias": macroAlias, - "syntax": syntax - }; - - return macroObject; - - } - - }; - -} - -angular.module('umbraco.services').factory('macroService', macroService); - -/** -* @ngdoc service -* @name umbraco.services.mediaHelper -* @description A helper object used for dealing with media items -**/ -function mediaHelper(umbRequestHelper) { - - //container of fileresolvers - var _mediaFileResolvers = {}; - - return { - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getImagePropertyValue - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns the file path associated with the media property if there is one - * - * @param {object} options Options object - * @param {object} options.mediaModel The media object to retrieve the image path from - * @param {object} options.imageOnly Optional, if true then will only return a path if the media item is an image + * @deprecated */ - getMediaPropertyValue: function (options) { - if (!options || !options.mediaModel) { - throw "The options objet does not contain the required parameters: mediaModel"; - } - - //combine all props, TODO: we really need a better way then this - var props = []; - if (options.mediaModel.properties) { - props = options.mediaModel.properties; - } else { - $(options.mediaModel.tabs).each(function (i, tab) { - props = props.concat(tab.properties); - }); + detectIfImageByExtension: function (imagePath) { + return mediaHelper.detectIfImageByExtension(imagePath); } - - var mediaRoot = Umbraco.Sys.ServerVariables.umbracoSettings.mediaPath; - var imageProp = _.find(props, function (item) { - if (item.alias === "umbracoFile") { - return true; - } - - //this performs a simple check to see if we have a media file as value - //it doesnt catch everything, but better then nothing - if (angular.isString(item.value) && item.value.indexOf(mediaRoot) === 0) { - return true; - } - - return false; - }); - - if (!imageProp) { - return ""; - } - - var mediaVal; - - //our default images might store one or many images (as csv) - var split = imageProp.value.split(','); - var self = this; - mediaVal = _.map(split, function (item) { - return { file: item, isImage: self.detectIfImageByExtension(item) }; - }); - - //for now we'll just return the first image in the collection. - //TODO: we should enable returning many to be displayed in the picker if the uploader supports many. - if (mediaVal.length && mediaVal.length > 0) { - if (!options.imageOnly || (options.imageOnly === true && mediaVal[0].isImage)) { - return mediaVal[0].file; - } - } - - return ""; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getImagePropertyValue - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns the actual image path associated with the image property if there is one - * - * @param {object} options Options object - * @param {object} options.imageModel The media object to retrieve the image path from - */ - getImagePropertyValue: function (options) { - if (!options || (!options.imageModel && !options.mediaModel)) { - throw "The options objet does not contain the required parameters: imageModel"; - } - - //required to support backwards compatibility. - options.mediaModel = options.imageModel ? options.imageModel : options.mediaModel; - - options.imageOnly = true; - - return this.getMediaPropertyValue(options); - }, - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getThumbnail - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * formats the display model used to display the content to the model used to save the content - * - * @param {object} options Options object - * @param {object} options.imageModel The media object to retrieve the image path from - */ - getThumbnail: function (options) { - - if (!options || !options.imageModel) { - throw "The options objet does not contain the required parameters: imageModel"; - } - - var imagePropVal = this.getImagePropertyValue(options); - if (imagePropVal !== "") { - return this.getThumbnailFromPath(imagePropVal); - } - return ""; - }, - - registerFileResolver: function(propertyEditorAlias, func){ - _mediaFileResolvers[propertyEditorAlias] = func; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#resolveFileFromEntity - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Gets the media file url for a media entity returned with the entityResource - * - * @param {object} mediaEntity A media Entity returned from the entityResource - * @param {boolean} thumbnail Whether to return the thumbnail url or normal url - */ - resolveFileFromEntity : function(mediaEntity, thumbnail) { - - if (!angular.isObject(mediaEntity.metaData)) { - throw "Cannot resolve the file url from the mediaEntity, it does not contain the required metaData"; - } - - var values = _.values(mediaEntity.metaData); - for (var i = 0; i < values.length; i++) { - var val = values[i]; - if (angular.isObject(val) && val.PropertyEditorAlias) { - for (var resolver in _mediaFileResolvers) { - if (val.PropertyEditorAlias === resolver) { - //we need to format a property variable that coincides with how the property would be structured - // if it came from the mediaResource just to keep things slightly easier for the file resolvers. - var property = { value: val.Value }; - - return _mediaFileResolvers[resolver](property, mediaEntity, thumbnail); - } - } - } + }; + } + angular.module('umbraco.services').factory('imageHelper', imageHelper); + // This service was based on OpenJS library available in BSD License + // http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php + function keyboardService($window, $timeout) { + var keyboardManagerService = {}; + var defaultOpt = { + 'type': 'keydown', + 'propagate': false, + 'inputDisabled': false, + 'target': $window.document, + 'keyCode': false + }; + // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken + var shift_nums = { + '`': '~', + '1': '!', + '2': '@', + '3': '#', + '4': '$', + '5': '%', + '6': '^', + '7': '&', + '8': '*', + '9': '(', + '0': ')', + '-': '_', + '=': '+', + ';': ':', + '\'': '"', + ',': '<', + '.': '>', + '/': '?', + '\\': '|' + }; + // Special Keys - and their codes + var special_keys = { + 'esc': 27, + 'escape': 27, + 'tab': 9, + 'space': 32, + 'return': 13, + 'enter': 13, + 'backspace': 8, + 'scrolllock': 145, + 'scroll_lock': 145, + 'scroll': 145, + 'capslock': 20, + 'caps_lock': 20, + 'caps': 20, + 'numlock': 144, + 'num_lock': 144, + 'num': 144, + 'pause': 19, + 'break': 19, + 'insert': 45, + 'home': 36, + 'delete': 46, + 'end': 35, + 'pageup': 33, + 'page_up': 33, + 'pu': 33, + 'pagedown': 34, + 'page_down': 34, + 'pd': 34, + 'left': 37, + 'up': 38, + 'right': 39, + 'down': 40, + 'f1': 112, + 'f2': 113, + 'f3': 114, + 'f4': 115, + 'f5': 116, + 'f6': 117, + 'f7': 118, + 'f8': 119, + 'f9': 120, + 'f10': 121, + 'f11': 122, + 'f12': 123 + }; + var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + // The event handler for bound element events + function eventHandler(e) { + e = e || $window.event; + var code, k; + // Find out which key is pressed + if (e.keyCode) { + code = e.keyCode; + } else if (e.which) { + code = e.which; } - - return ""; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#resolveFile - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Gets the media file url for a media object returned with the mediaResource - * - * @param {object} mediaEntity A media Entity returned from the entityResource - * @param {boolean} thumbnail Whether to return the thumbnail url or normal url - */ - /*jshint loopfunc: true */ - resolveFile : function(mediaItem, thumbnail){ - - function iterateProps(props){ - var res = null; - for(var resolver in _mediaFileResolvers) { - var property = _.find(props, function(prop){ return prop.editor === resolver; }); - if(property){ - res = _mediaFileResolvers[resolver](property, mediaItem, thumbnail); + var character = String.fromCharCode(code).toLowerCase(); + if (code === 188) { + character = ','; + } + // If the user presses , when the type is onkeydown + if (code === 190) { + character = '.'; + } + // If the user presses , when the type is onkeydown + var propagate = true; + //Now we need to determine which shortcut this event is for, we'll do this by iterating over each + //registered shortcut to find the match. We use Find here so that the loop exits as soon + //as we've found the one we're looking for + _.find(_.keys(keyboardManagerService.keyboardEvent), function (key) { + var shortcutLabel = key; + var shortcutVal = keyboardManagerService.keyboardEvent[key]; + // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked + var kp = 0; + // Some modifiers key + var modifiers = { + shift: { + wanted: false, + pressed: e.shiftKey ? true : false + }, + ctrl: { + wanted: false, + pressed: e.ctrlKey ? true : false + }, + alt: { + wanted: false, + pressed: e.altKey ? true : false + }, + meta: { + //Meta is Mac specific + wanted: false, + pressed: e.metaKey ? true : false + } + }; + var keys = shortcutLabel.split('+'); + var opt = shortcutVal.opt; + var callback = shortcutVal.callback; + // Foreach keys in label (split on +) + var l = keys.length; + for (var i = 0; i < l; i++) { + var k = keys[i]; + switch (k) { + case 'ctrl': + case 'control': + kp++; + modifiers.ctrl.wanted = true; + break; + case 'shift': + case 'alt': + case 'meta': + kp++; + modifiers[k].wanted = true; break; } - } - - return res; - } - - //we either have properties raw on the object, or spread out on tabs - var result = ""; - if(mediaItem.properties){ - result = iterateProps(mediaItem.properties); - }else if(mediaItem.tabs){ - for(var tab in mediaItem.tabs) { - if(mediaItem.tabs[tab].properties){ - result = iterateProps(mediaItem.tabs[tab].properties); - if(result){ - break; + if (k.length > 1) { + // If it is a special key + if (special_keys[k] === code) { + kp++; + } + } else if (opt['keyCode']) { + // If a specific key is set into the config + if (opt['keyCode'] === code) { + kp++; + } + } else { + // The special keys did not match + if (character === k) { + kp++; + } else { + if (shift_nums[character] && e.shiftKey) { + // Stupid Shift key bug created by using lowercase + character = shift_nums[character]; + if (character === k) { + kp++; + } + } } } } - } - return result; - }, - - /*jshint loopfunc: true */ - hasFilePropertyType : function(mediaItem){ - function iterateProps(props){ - var res = false; - for(var resolver in _mediaFileResolvers) { - var property = _.find(props, function(prop){ return prop.editor === resolver; }); - if(property){ - res = true; - break; - } - } - return res; - } - - //we either have properties raw on the object, or spread out on tabs - var result = false; - if(mediaItem.properties){ - result = iterateProps(mediaItem.properties); - }else if(mediaItem.tabs){ - for(var tab in mediaItem.tabs) { - if(mediaItem.tabs[tab].properties){ - result = iterateProps(mediaItem.tabs[tab].properties); - if(result){ - break; - } - } - } - } - return result; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#scaleToMaxSize - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Finds the corrct max width and max height, given maximum dimensions and keeping aspect ratios - * - * @param {number} maxSize Maximum width & height - * @param {number} width Current width - * @param {number} height Current height - */ - scaleToMaxSize: function (maxSize, width, height) { - var retval = { width: width, height: height }; - - var maxWidth = maxSize; // Max width for the image - var maxHeight = maxSize; // Max height for the image - var ratio = 0; // Used for aspect ratio - - // Check if the current width is larger than the max - if (width > maxWidth) { - ratio = maxWidth / width; // get ratio for scaling image - - retval.width = maxWidth; - retval.height = height * ratio; - - height = height * ratio; // Reset height to match scaled image - width = width * ratio; // Reset width to match scaled image - } - - // Check if current height is larger than max - if (height > maxHeight) { - ratio = maxHeight / height; // get ratio for scaling image - - retval.height = maxHeight; - retval.width = width * ratio; - width = width * ratio; // Reset width to match scaled image - } - - return retval; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getThumbnailFromPath - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns the path to the thumbnail version of a given media library image path - * - * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg - */ - getThumbnailFromPath: function (imagePath) { - - //If the path is not an image we cannot get a thumb - if (!this.detectIfImageByExtension(imagePath)) { - return null; - } - - //get the proxy url for big thumbnails (this ensures one is always generated) - var thumbnailUrl = umbRequestHelper.getApiUrl( - "imagesApiBaseUrl", - "GetBigThumbnail", - [{ originalImagePath: imagePath }]); - - //var ext = imagePath.substr(imagePath.lastIndexOf('.')); - //return imagePath.substr(0, imagePath.lastIndexOf('.')) + "_big-thumb" + ".jpg"; - - return thumbnailUrl; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#detectIfImageByExtension - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns true/false, indicating if the given path has an allowed image extension - * - * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg - */ - detectIfImageByExtension: function (imagePath) { - - if (!imagePath) { - return false; - } - - var lowered = imagePath.toLowerCase(); - var ext = lowered.substr(lowered.lastIndexOf(".") + 1); - return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#formatFileTypes - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns a string with correctly formated file types for ng-file-upload - * - * @param {string} file types, ex: jpg,png,tiff - */ - formatFileTypes: function(fileTypes) { - - var fileTypesArray = fileTypes.split(','); - var newFileTypesArray = []; - - for (var i = 0; i < fileTypesArray.length; i++) { - var fileType = fileTypesArray[i]; - - if (fileType.indexOf(".") !== 0) { - fileType = ".".concat(fileType); - } - - newFileTypesArray.push(fileType); - } - - return newFileTypesArray.join(","); - - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getFileExtension - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns file extension - * - * @param {string} filePath File path, ex /media/1234/my-image.jpg - */ - getFileExtension: function(filePath) { - - if (!filePath) { - return false; - } - - var lowered = filePath.toLowerCase(); - var ext = lowered.substr(lowered.lastIndexOf(".") + 1); - return ext; - } - - }; -}angular.module('umbraco.services').factory('mediaHelper', mediaHelper); - -/** - * @ngdoc service - * @name umbraco.services.mediaTypeHelper - * @description A helper service for the media types - **/ -function mediaTypeHelper(mediaTypeResource, $q) { - - var mediaTypeHelperService = { - - isFolderType: function(mediaEntity) { - if (!mediaEntity) { - throw "mediaEntity is null"; - } - if (!mediaEntity.contentTypeAlias) { - throw "mediaEntity.contentTypeAlias is null"; - } - - //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" - //this is the exact same logic that is performed in MediaController.GetChildFolders - return mediaEntity.contentTypeAlias.endsWith("Folder"); - }, - - getAllowedImagetypes: function (mediaId){ - - //TODO: This is horribly inneficient - why make one request per type!? - - // Get All allowedTypes - return mediaTypeResource.getAllowedTypes(mediaId) - .then(function(types){ - - var allowedQ = types.map(function(type){ - return mediaTypeResource.getById(type.id); - }); - - // Get full list - return $q.all(allowedQ).then(function(fullTypes){ - - // Find all the media types with an Image Cropper property editor - var filteredTypes = mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper']); - - // If there is only one media type with an Image Cropper we will return this one - if(filteredTypes.length === 1) { - return filteredTypes; - // If there is more than one Image cropper, custom media types have been added, and we return all media types with and Image cropper or UploadField - } else { - return mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper', 'Umbraco.UploadField']); + //for end + if (kp === keys.length && modifiers.ctrl.pressed === modifiers.ctrl.wanted && modifiers.shift.pressed === modifiers.shift.wanted && modifiers.alt.pressed === modifiers.alt.wanted && modifiers.meta.pressed === modifiers.meta.wanted) { + //found the right callback! + // Disable event handler when focus input and textarea + if (opt['inputDisabled']) { + var elt; + if (e.target) { + elt = e.target; + } else if (e.srcElement) { + elt = e.srcElement; } - - }); - }); - }, - - getTypeWithEditor: function (types, editors) { - - return types.filter(function (mediatype) { - for (var i = 0; i < mediatype.groups.length; i++) { - var group = mediatype.groups[i]; - for (var j = 0; j < group.properties.length; j++) { - var property = group.properties[j]; - if( editors.indexOf(property.editor) !== -1 ) { - return mediatype; + if (elt.nodeType === 3) { + elt = elt.parentNode; + } + if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA') { + //This exits the Find loop + return true; } } + $timeout(function () { + callback(e); + }, 1); + if (!opt['propagate']) { + // Stop the event + propagate = false; + } + //This exits the Find loop + return true; } + //we haven't found one so continue looking + return false; }); - - } - - }; - - return mediaTypeHelperService; -} -angular.module('umbraco.services').factory('mediaTypeHelper', mediaTypeHelper); - -/** - * @ngdoc service - * @name umbraco.services.umbracoMenuActions - * - * @requires q - * @requires treeService - * - * @description - * Defines the methods that are called when menu items declare only an action to execute - */ -function umbracoMenuActions($q, treeService, $location, navigationService, appState) { - - return { - - /** - * @ngdoc method - * @name umbraco.services.umbracoMenuActions#RefreshNode - * @methodOf umbraco.services.umbracoMenuActions - * @function - * - * @description - * Clears all node children and then gets it's up-to-date children from the server and re-assigns them - * @param {object} args An arguments object - * @param {object} args.entity The basic entity being acted upon - * @param {object} args.treeAlias The tree alias associated with this entity - * @param {object} args.section The current section - */ - "RefreshNode": function (args) { - - ////just in case clear any tree cache for this node/section - //treeService.clearCache({ - // cacheKey: "__" + args.section, //each item in the tree cache is cached by the section name - // childrenOf: args.entity.parentId //clear the children of the parent - //}); - - //since we're dealing with an entity, we need to attempt to find it's tree node, in the main tree - // this action is purely a UI thing so if for whatever reason there is no loaded tree node in the UI - // we can safely ignore this process. - - //to find a visible tree node, we'll go get the currently loaded root node from appState - var treeRoot = appState.getTreeState("currentRootNode"); - if (treeRoot && treeRoot.root) { - var treeNode = treeService.getDescendantNode(treeRoot.root, args.entity.id, args.treeAlias); - if (treeNode) { - treeService.loadNodeChildren({ node: treeNode, section: args.section }); - } - } - - - }, - - /** - * @ngdoc method - * @name umbraco.services.umbracoMenuActions#CreateChildEntity - * @methodOf umbraco.services.umbracoMenuActions - * @function - * - * @description - * This will re-route to a route for creating a new entity as a child of the current node - * @param {object} args An arguments object - * @param {object} args.entity The basic entity being acted upon - * @param {object} args.treeAlias The tree alias associated with this entity - * @param {object} args.section The current section - */ - "CreateChildEntity": function (args) { - - navigationService.hideNavigation(); - - var route = "/" + args.section + "/" + args.treeAlias + "/edit/" + args.entity.id; - //change to new path - $location.path(route).search({ create: true }); - - } - }; -} - -angular.module('umbraco.services').factory('umbracoMenuActions', umbracoMenuActions); -(function () { - 'use strict'; - - function miniEditorHelper(dialogService, editorState, fileManager, contentEditingHelper, $q) { - - var launched = false; - - function launchMiniEditor(node) { - - var deferred = $q.defer(); - - launched = true; - - //We need to store the current files selected in the file manager locally because the fileManager - // is a singleton and is shared globally. The mini dialog will also be referencing the fileManager - // and we don't want it to be sharing the same files as the main editor. So we'll store the current files locally here, - // clear them out and then launch the dialog. When the dialog closes, we'll reset the fileManager to it's previous state. - var currFiles = _.groupBy(fileManager.getFiles(), "alias"); - fileManager.clearFiles(); - - //We need to store the original editorState entity because it will need to change when the mini editor is loaded so that - // any property editors that are working with editorState get given the correct entity, otherwise strange things will - // start happening. - var currEditorState = editorState.getCurrent(); - - dialogService.open({ - template: "views/common/dialogs/content/edit.html", - id: node.id, - closeOnSave: true, - tabFilter: ["Generic properties"], - callback: function (data) { - - //set the node name back - node.name = data.name; - - //reset the fileManager to what it was - fileManager.clearFiles(); - _.each(currFiles, function (val, key) { - fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); - }); - - //reset the editor state - editorState.set(currEditorState); - - //Now we need to check if the content item that was edited was actually the same content item - // as the main content editor and if so, update all property data - if (data.id === currEditorState.id) { - var changed = contentEditingHelper.reBindChangedProperties(currEditorState, data); - } - - launched = false; - - deferred.resolve(data); - - }, - closeCallback: function () { - //reset the fileManager to what it was - fileManager.clearFiles(); - _.each(currFiles, function (val, key) { - fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); - }); - - //reset the editor state - editorState.set(currEditorState); - - launched = false; - - deferred.reject(); - + // Stop the event if required + if (!propagate) { + // e.cancelBubble is supported by IE - this will kill the bubbling process. + e.cancelBubble = true; + e.returnValue = false; + // e.stopPropagation works in Firefox. + if (e.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); } - }); - - return deferred.promise; - + return false; + } } - - var service = { - launchMiniEditor: launchMiniEditor + // Store all keyboard combination shortcuts + keyboardManagerService.keyboardEvent = {}; + // Add a new keyboard combination shortcut + keyboardManagerService.bind = function (label, callback, opt) { + //replace ctrl key with meta key + if (isMac && label !== 'ctrl+space') { + label = label.replace('ctrl', 'meta'); + } + var elt; + // Initialize opt object + opt = angular.extend({}, defaultOpt, opt); + label = label.toLowerCase(); + elt = opt.target; + if (typeof opt.target === 'string') { + elt = document.getElementById(opt.target); + } + //Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding + // and raising events for now reason. So here we'll check if the event is already registered for the element + var boundValues = _.values(keyboardManagerService.keyboardEvent); + var found = _.find(boundValues, function (i) { + return i.target === elt && i.event === opt['type']; + }); + // Store shortcut + keyboardManagerService.keyboardEvent[label] = { + 'callback': callback, + 'target': elt, + 'opt': opt + }; + if (!found) { + //Attach the function with the event + if (elt.addEventListener) { + elt.addEventListener(opt['type'], eventHandler, false); + } else if (elt.attachEvent) { + elt.attachEvent('on' + opt['type'], eventHandler); + } else { + elt['on' + opt['type']] = eventHandler; + } + } }; - - return service; - + // Remove the shortcut - just specify the shortcut and I will remove the binding + keyboardManagerService.unbind = function (label) { + label = label.toLowerCase(); + var binding = keyboardManagerService.keyboardEvent[label]; + delete keyboardManagerService.keyboardEvent[label]; + if (!binding) { + return; + } + var type = binding['event'], elt = binding['target'], callback = binding['callback']; + if (elt.detachEvent) { + elt.detachEvent('on' + type, callback); + } else if (elt.removeEventListener) { + elt.removeEventListener(type, callback, false); + } else { + elt['on' + type] = false; + } + }; + // + return keyboardManagerService; } - - - angular.module('umbraco.services').factory('miniEditorHelper', miniEditorHelper); - - -})(); - -/** - * @ngdoc service - * @name umbraco.services.navigationService - * - * @requires $rootScope - * @requires $routeParams - * @requires $log - * @requires $location - * @requires dialogService - * @requires treeService - * @requires sectionResource - * - * @description - * Service to handle the main application navigation. Responsible for invoking the tree - * Section navigation and search, and maintain their state for the entire application lifetime - * + angular.module('umbraco.services').factory('keyboardService', [ + '$window', + '$timeout', + keyboardService + ]); + /** + @ngdoc service + * @name umbraco.services.listViewHelper + * + * + * @description + * Service for performing operations against items in the list view UI. Used by the built-in internal listviews + * as well as custom listview. + * + * A custom listview is always used inside a wrapper listview, so there are a number of inherited values on its + * scope by default: + * + * **$scope.selection**: Array containing all items currently selected in the listview + * + * **$scope.items**: Array containing all items currently displayed in the listview + * + * **$scope.folders**: Array containing all folders in the current listview (only for media) + * + * **$scope.options**: configuration object containing information such as pagesize, permissions, order direction etc. + * + * **$scope.model.config.layouts**: array of available layouts to apply to the listview (grid, list or custom layout) + * + * ##Usage## + * To use, inject listViewHelper into custom listview controller, listviewhelper expects you + * to pass in the full collection of items in the listview in several of its methods + * this collection is inherited from the parent controller and is available on $scope.selection + * + *
    + *      angular.module("umbraco").controller("my.listVieweditor". function($scope, listViewHelper){
    + *
    + *          //current items in the listview
    + *          var items = $scope.items;
    + *
    + *          //current selection
    + *          var selection = $scope.selection;
    + *
    + *          //deselect an item , $scope.selection is inherited, item is picked from inherited $scope.items
    + *          listViewHelper.deselectItem(item, $scope.selection);
    + *
    + *          //test if all items are selected, $scope.items + $scope.selection are inherited
    + *          listViewhelper.isSelectedAll($scope.items, $scope.selection);
    + *      });
    + * 
    */ -function navigationService($rootScope, $routeParams, $log, $location, $q, $timeout, $injector, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) { - - - //used to track the current dialog object - var currentDialog = null; - - //the main tree event handler, which gets assigned via the setupTreeEvents method - var mainTreeEventHandler = null; - //tracks the user profile dialog - var userDialog = null; - - function setMode(mode) { - switch (mode) { - case 'tree': - appState.setGlobalState("navMode", "tree"); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", false); - appState.setMenuState("showMenuDialog", false); - appState.setGlobalState("stickyNavigation", false); - appState.setGlobalState("showTray", false); - - //$("#search-form input").focus(); - break; - case 'menu': - appState.setGlobalState("navMode", "menu"); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", true); - appState.setMenuState("showMenuDialog", false); - appState.setGlobalState("stickyNavigation", true); - break; - case 'dialog': - appState.setGlobalState("navMode", "dialog"); - appState.setGlobalState("stickyNavigation", true); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", false); - appState.setMenuState("showMenuDialog", true); - break; - case 'search': - appState.setGlobalState("navMode", "search"); - appState.setGlobalState("stickyNavigation", false); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", false); - appState.setSectionState("showSearchResults", true); - appState.setMenuState("showMenuDialog", false); - - //TODO: This would be much better off in the search field controller listening to appState changes - $timeout(function() { - $("#search-field").focus(); - }); - - break; - default: - appState.setGlobalState("navMode", "default"); - appState.setMenuState("showMenu", false); - appState.setMenuState("showMenuDialog", false); - appState.setSectionState("showSearchResults", false); - appState.setGlobalState("stickyNavigation", false); - appState.setGlobalState("showTray", false); - - if (appState.getGlobalState("isTablet") === true) { - appState.setGlobalState("showNavigation", false); - } - - break; - } - } - - var service = { - - /** initializes the navigation service */ - init: function() { - - //keep track of the current section - initially this will always be undefined so - // no point in setting it now until it changes. - $rootScope.$watch(function () { - return $routeParams.section; - }, function (newVal, oldVal) { - appState.setSectionState("currentSection", newVal); - }); - - - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#load - * @methodOf umbraco.services.navigationService - * - * @description - * Shows the legacy iframe and loads in the content based on the source url - * @param {String} source The URL to load into the iframe - */ - loadLegacyIFrame: function (source) { - $location.path("/" + appState.getSectionState("currentSection") + "/framed/" + encodeURIComponent(source)); - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#changeSection - * @methodOf umbraco.services.navigationService - * - * @description - * Changes the active section to a given section alias - * If the navigation is 'sticky' this will load the associated tree - * and load the dashboard related to the section - * @param {string} sectionAlias The alias of the section - */ - changeSection: function(sectionAlias, force) { - setMode("default-opensection"); - - if (force && appState.getSectionState("currentSection") === sectionAlias) { - appState.setSectionState("currentSection", ""); - } - - appState.setSectionState("currentSection", sectionAlias); - this.showTree(sectionAlias); - - $location.path(sectionAlias); - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showTree - * @methodOf umbraco.services.navigationService - * - * @description - * Displays the tree for a given section alias but turning on the containing dom element - * only changes if the section is different from the current one - * @param {string} sectionAlias The alias of the section to load - * @param {Object} syncArgs Optional object of arguments for syncing the tree for the section being shown - */ - showTree: function (sectionAlias, syncArgs) { - if (sectionAlias !== appState.getSectionState("currentSection")) { - appState.setSectionState("currentSection", sectionAlias); - - if (syncArgs) { - this.syncTree(syncArgs); - } - } - setMode("tree"); - }, - - showTray: function () { - appState.setGlobalState("showTray", true); - }, - - hideTray: function () { - appState.setGlobalState("showTray", false); - }, - - /** - Called to assign the main tree event handler - this is called by the navigation controller. - TODO: Potentially another dev could call this which would kind of mung the whole app so potentially there's a better way. + (function () { + 'use strict'; + function listViewHelper(localStorageService) { + var firstSelectedIndex = 0; + var localStorageKey = 'umblistViewLayout'; + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#getLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method for internal use, based on the collection of layouts passed, the method selects either + * any previous layout from local storage, or picks the first allowed layout + * + * @param {Number} nodeId The id of the current node displayed in the content editor + * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts */ - setupTreeEvents: function(treeEventHandler) { - mainTreeEventHandler = treeEventHandler; - - //when a tree is loaded into a section, we need to put it into appState - mainTreeEventHandler.bind("treeLoaded", function(ev, args) { - appState.setTreeState("currentRootNode", args.tree); - }); - - //when a tree node is synced this event will fire, this allows us to set the currentNode - mainTreeEventHandler.bind("treeSynced", function (ev, args) { - - if (args.activate === undefined || args.activate === true) { - //set the current selected node - appState.setTreeState("selectedNode", args.node); - //when a node is activated, this is the same as clicking it and we need to set the - //current menu item to be this node as well. - appState.setMenuState("currentNode", args.node); - } - }); - - //this reacts to the options item in the tree - mainTreeEventHandler.bind("treeOptionsClick", function(ev, args) { - ev.stopPropagation(); - ev.preventDefault(); - - //Set the current action node (this is not the same as the current selected node!) - appState.setMenuState("currentNode", args.node); - - if (args.event && args.event.altKey) { - args.skipDefault = true; + function getLayout(nodeId, availableLayouts) { + var storedLayouts = []; + if (localStorageService.get(localStorageKey)) { + storedLayouts = localStorageService.get(localStorageKey); } - - service.showMenu(ev, args); - }); - - mainTreeEventHandler.bind("treeNodeAltSelect", function(ev, args) { - ev.stopPropagation(); - ev.preventDefault(); - - args.skipDefault = true; - service.showMenu(ev, args); - }); - - //this reacts to tree items themselves being clicked - //the tree directive should not contain any handling, simply just bubble events - mainTreeEventHandler.bind("treeNodeSelect", function (ev, args) { - var n = args.node; - ev.stopPropagation(); - ev.preventDefault(); - - if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { - //this is a legacy tree node! - var jsPrefix = "javascript:"; - var js; - if (n.metaData["jsClickCallback"].startsWith(jsPrefix)) { - js = n.metaData["jsClickCallback"].substr(jsPrefix.length); - } - else { - js = n.metaData["jsClickCallback"]; - } - try { - var func = eval(js); - //this is normally not necessary since the eval above should execute the method and will return nothing. - if (func != null && (typeof func === "function")) { - func.call(); + if (storedLayouts && storedLayouts.length > 0) { + for (var i = 0; storedLayouts.length > i; i++) { + var layout = storedLayouts[i]; + if (layout.nodeId === nodeId) { + return setLayout(nodeId, layout, availableLayouts); } } - catch(ex) { - $log.error("Error evaluating js callback from legacy tree node: " + ex); + } + return getFirstAllowedLayout(availableLayouts); + } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage + * + * @param {Number} nodeID Id of the current node displayed in the content editor + * @param {Object} selectedLayout Layout selected as the layout to set as the current layout + * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + function setLayout(nodeId, selectedLayout, availableLayouts) { + var activeLayout = {}; + var layoutFound = false; + for (var i = 0; availableLayouts.length > i; i++) { + var layout = availableLayouts[i]; + if (layout.path === selectedLayout.path) { + activeLayout = layout; + layout.active = true; + layoutFound = true; + } else { + layout.active = false; } } - else if (n.routePath) { - //add action to the history service - historyService.add({ name: n.name, link: n.routePath, icon: n.icon }); - - //put this node into the tree state - appState.setTreeState("selectedNode", args.node); - //when a node is clicked we also need to set the active menu node to this node - appState.setMenuState("currentNode", args.node); - - //not legacy, lets just set the route value and clear the query string if there is one. - $location.path(n.routePath).search(""); + if (!layoutFound) { + activeLayout = getFirstAllowedLayout(availableLayouts); + } + saveLayoutInLocalStorage(nodeId, activeLayout); + return activeLayout; + } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#saveLayoutInLocalStorage + * @methodOf umbraco.services.listViewHelper + * + * @description + * Stores a given layout as the current default selection in local storage + * + * @param {Number} nodeId Id of the current node displayed in the content editor + * @param {Object} selectedLayout Layout selected as the layout to set as the current layout + */ + function saveLayoutInLocalStorage(nodeId, selectedLayout) { + var layoutFound = false; + var storedLayouts = []; + if (localStorageService.get(localStorageKey)) { + storedLayouts = localStorageService.get(localStorageKey); + } + if (storedLayouts.length > 0) { + for (var i = 0; storedLayouts.length > i; i++) { + var layout = storedLayouts[i]; + if (layout.nodeId === nodeId) { + layout.path = selectedLayout.path; + layoutFound = true; + } + } } - else if (args.element.section) { - $location.path(args.element.section).search(""); + if (!layoutFound) { + var storageObject = { + 'nodeId': nodeId, + 'path': selectedLayout.path + }; + storedLayouts.push(storageObject); } - - service.hideNavigation(); - }); - }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#syncTree - * @methodOf umbraco.services.navigationService - * - * @description - * Syncs a tree with a given path, returns a promise - * The path format is: ["itemId","itemId"], and so on - * so to sync to a specific document type node do: - *
    -         * navigationService.syncTree({tree: 'content', path: ["-1","123d"], forceReload: true});
    -         * 
    - * @param {Object} args arguments passed to the function - * @param {String} args.tree the tree alias to sync to - * @param {Array} args.path the path to sync the tree to - * @param {Boolean} args.forceReload optional, specifies whether to force reload the node data from the server even if it already exists in the tree currently - * @param {Boolean} args.activate optional, specifies whether to set the synced node to be the active node, this will default to true if not specified - */ - syncTree: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.path) { - throw "args.path cannot be null"; - } - if (!args.tree) { - throw "args.tree cannot be null"; - } - - if (mainTreeEventHandler) { - //returns a promise - return mainTreeEventHandler.syncTree(args); - } - - //couldn't sync - return angularHelper.rejectedPromise(); - }, - - /** - Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to - have to set an active tree and then sync, the new API does this in one method by using syncTree + localStorageService.set(localStorageKey, storedLayouts); + } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#getFirstAllowedLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Returns currently selected layout, or alternatively the first layout in the available layouts collection + * + * @param {Array} layouts Array of all allowed layouts, available from $scope.model.config.layouts */ - _syncPath: function(path, forceReload) { - if (mainTreeEventHandler) { - mainTreeEventHandler.syncTree({ path: path, forceReload: forceReload }); - } - }, - - //TODO: This should return a promise - reloadNode: function(node) { - if (mainTreeEventHandler) { - mainTreeEventHandler.reloadNode(node); - } - }, - - //TODO: This should return a promise - reloadSection: function(sectionAlias) { - if (mainTreeEventHandler) { - mainTreeEventHandler.clearCache({ section: sectionAlias }); - mainTreeEventHandler.load(sectionAlias); - } - }, - - /** - Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to - have to set an active tree and then sync, the new API does this in one method by using syncTreePath + function getFirstAllowedLayout(layouts) { + var firstAllowedLayout = {}; + for (var i = 0; layouts.length > i; i++) { + var layout = layouts[i]; + if (layout.selected === true) { + firstAllowedLayout = layout; + break; + } + } + return firstAllowedLayout; + } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectHandler + * @methodOf umbraco.services.listViewHelper + * + * @description + * Helper method for working with item selection via a checkbox, internally it uses selectItem and deselectItem. + * Working with this method, requires its triggered via a checkbox which can then pass in its triggered $event + * When the checkbox is clicked, this method will toggle selection of the associated item so it matches the state of the checkbox + * + * @param {Object} selectedItem Item being selected or deselected by the checkbox + * @param {Number} selectedIndex Index of item being selected/deselected, usually passed as $index + * @param {Array} items All items in the current listview, available as $scope.items + * @param {Array} selection All selected items in the current listview, available as $scope.selection + * @param {Event} $event Event triggered by the checkbox being checked to select / deselect an item */ - _setActiveTreeType: function (treeAlias, loadChildren) { - if (mainTreeEventHandler) { - mainTreeEventHandler._setActiveTreeType(treeAlias, loadChildren); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideTree - * @methodOf umbraco.services.navigationService - * - * @description - * Hides the tree by hiding the containing dom element - */ - hideTree: function() { - - if (appState.getGlobalState("isTablet") === true && !appState.getGlobalState("stickyNavigation")) { - //reset it to whatever is in the url - appState.setSectionState("currentSection", $routeParams.section); - setMode("default-hidesectiontree"); - } - - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showMenu - * @methodOf umbraco.services.navigationService - * - * @description - * Hides the tree by hiding the containing dom element. - * This always returns a promise! - * - * @param {Event} event the click event triggering the method, passed from the DOM element - */ - showMenu: function(event, args) { - - var deferred = $q.defer(); - var self = this; - - treeService.getMenu({ treeNode: args.node }) - .then(function(data) { - - //check for a default - //NOTE: event will be undefined when a call to hideDialog is made so it won't re-load the default again. - // but perhaps there's a better way to deal with with an additional parameter in the args ? it works though. - if (data.defaultAlias && !args.skipDefault) { - - var found = _.find(data.menuItems, function(item) { - return item.alias = data.defaultAlias; - }); - - if (found) { - - //NOTE: This is assigning the current action node - this is not the same as the currently selected node! - appState.setMenuState("currentNode", args.node); - - //ensure the current dialog is cleared before creating another! - if (currentDialog) { - dialogService.close(currentDialog); - } - - var dialog = self.showDialog({ - node: args.node, - action: found, - section: appState.getSectionState("currentSection") - }); - - //return the dialog this is opening. - deferred.resolve(dialog); - return; + function selectHandler(selectedItem, selectedIndex, items, selection, $event) { + var start = 0; + var end = 0; + var item = null; + if ($event.shiftKey === true) { + if (selectedIndex > firstSelectedIndex) { + start = firstSelectedIndex; + end = selectedIndex; + for (; end >= start; start++) { + item = items[start]; + selectItem(item, selection); + } + } else { + start = firstSelectedIndex; + end = selectedIndex; + for (; end <= start; start--) { + item = items[start]; + selectItem(item, selection); } } - - //there is no default or we couldn't find one so just continue showing the menu - - setMode("menu"); - - appState.setMenuState("currentNode", args.node); - appState.setMenuState("menuActions", data.menuItems); - appState.setMenuState("dialogTitle", args.node.name); - - //we're not opening a dialog, return null. - deferred.resolve(null); - }); - - return deferred.promise; - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideMenu - * @methodOf umbraco.services.navigationService - * - * @description - * Hides the menu by hiding the containing dom element - */ - hideMenu: function() { - //SD: Would we ever want to access the last action'd node instead of clearing it here? - appState.setMenuState("currentNode", null); - appState.setMenuState("menuActions", []); - setMode("tree"); - }, - - /** Executes a given menu action */ - executeMenuAction: function (action, node, section) { - - if (!action) { - throw "action cannot be null"; - } - if (!node) { - throw "node cannot be null"; - } - if (!section) { - throw "section cannot be null"; - } - - if (action.metaData && action.metaData["actionRoute"] && angular.isString(action.metaData["actionRoute"])) { - //first check if the menu item simply navigates to a route - var parts = action.metaData["actionRoute"].split("?"); - $location.path(parts[0]).search(parts.length > 1 ? parts[1] : ""); - this.hideNavigation(); - return; + } else { + if (selectedItem.selected) { + deselectItem(selectedItem, selection); + } else { + selectItem(selectedItem, selection); + } + firstSelectedIndex = selectedIndex; + } } - else if (action.metaData && action.metaData["jsAction"] && angular.isString(action.metaData["jsAction"])) { - - //we'll try to get the jsAction from the injector - var menuAction = action.metaData["jsAction"].split('.'); - if (menuAction.length !== 2) { - - //if it is not two parts long then this most likely means that it's a legacy action - var js = action.metaData["jsAction"].replace("javascript:", ""); - //there's not really a different way to acheive this except for eval - eval(js); - } - else { - var menuActionService = $injector.get(menuAction[0]); - if (!menuActionService) { - throw "The angular service " + menuAction[0] + " could not be found"; - } - - var method = menuActionService[menuAction[1]]; - - if (!method) { - throw "The method " + menuAction[1] + " on the angular service " + menuAction[0] + " could not be found"; - } - - method.apply(this, [{ - //map our content object to a basic entity to pass in to the menu handlers, - //this is required for consistency since a menu item needs to be decoupled from a tree node since the menu can - //exist standalone in the editor for which it can only pass in an entity (not tree node). - entity: umbModelMapper.convertToEntityBasic(node), - action: action, - section: section, - treeAlias: treeService.getTreeAlias(node) - }]); + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Selects a given item to the listview selection array, requires you pass in the inherited $scope.selection collection + * + * @param {Object} item Item to select + * @param {Array} selection Listview selection, available as $scope.selection + */ + function selectItem(item, selection) { + var isSelected = false; + for (var i = 0; selection.length > i; i++) { + var selectedItem = selection[i]; + // if item.id is 2147483647 (int.MaxValue) use item.key + if (item.id !== 2147483647 && item.id === selectedItem.id || item.key === selectedItem.key) { + isSelected = true; + } + } + if (!isSelected) { + selection.push({ + id: item.id, + key: item.key + }); + item.selected = true; } } - else { - service.showDialog({ - node: node, - action: action, - section: section - }); + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#deselectItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Deselects a given item from the listviews selection array, requires you pass in the inherited $scope.selection collection + * + * @param {Object} item Item to deselect + * @param {Array} selection Listview selection, available as $scope.selection + */ + function deselectItem(item, selection) { + for (var i = 0; selection.length > i; i++) { + var selectedItem = selection[i]; + // if item.id is 2147483647 (int.MaxValue) use item.key + if (item.id !== 2147483647 && item.id === selectedItem.id || item.key === selectedItem.key) { + selection.splice(i, 1); + item.selected = false; + } + } } - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showUserDialog - * @methodOf umbraco.services.navigationService - * - * @description - * Opens the user dialog, next to the sections navigation - * template is located in views/common/dialogs/user.html - */ - showUserDialog: function () { - // hide tray and close help dialog - if (service.helpDialog) { - service.helpDialog.close(); - } - service.hideTray(); - - if (service.userDialog) { - service.userDialog.close(); - service.userDialog = undefined; - } - - service.userDialog = dialogService.open( - { - template: "views/common/dialogs/user.html", - modalClass: "umb-modal-left", - show: true - }); - - return service.userDialog; - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showUserDialog - * @methodOf umbraco.services.navigationService - * - * @description - * Opens the user dialog, next to the sections navigation - * template is located in views/common/dialogs/user.html - */ - showHelpDialog: function () { - // hide tray and close user dialog - service.hideTray(); - if (service.userDialog) { - service.userDialog.close(); - } - - if(service.helpDialog){ - service.helpDialog.close(); - service.helpDialog = undefined; - } - - service.helpDialog = dialogService.open( - { - template: "views/common/dialogs/help.html", - modalClass: "umb-modal-left", - show: true - }); - - return service.helpDialog; - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showDialog - * @methodOf umbraco.services.navigationService - * - * @description - * Opens a dialog, for a given action on a given tree node - * uses the dialogService to inject the selected action dialog - * into #dialog div.umb-panel-body - * the path to the dialog view is determined by: - * "views/" + current tree + "/" + action alias + ".html" - * The dialog controller will get passed a scope object that is created here with the properties: - * scope.currentNode = the selected tree node - * scope.currentAction = the selected menu item - * so that the dialog controllers can use these properties - * - * @param {Object} args arguments passed to the function - * @param {Scope} args.scope current scope passed to the dialog - * @param {Object} args.action the clicked action containing `name` and `alias` - */ - showDialog: function(args) { - - if (!args) { - throw "showDialog is missing the args parameter"; - } - if (!args.action) { - throw "The args parameter must have an 'action' property as the clicked menu action object"; - } - if (!args.node) { - throw "The args parameter must have a 'node' as the active tree node"; - } - - //ensure the current dialog is cleared before creating another! - if (currentDialog) { - dialogService.close(currentDialog); - currentDialog = null; - } - - setMode("dialog"); - - //NOTE: Set up the scope object and assign properties, this is legacy functionality but we have to live with it now. - // we should be passing in currentNode and currentAction using 'dialogData' for the dialog, not attaching it to a scope. - // This scope instance will be destroyed by the dialog so it cannot be a scope that exists outside of the dialog. - // If a scope instance has been passed in, we'll have to create a child scope of it, otherwise a new root scope. - var dialogScope = args.scope ? args.scope.$new() : $rootScope.$new(); - dialogScope.currentNode = args.node; - dialogScope.currentAction = args.action; - - //the title might be in the meta data, check there first - if (args.action.metaData["dialogTitle"]) { - appState.setMenuState("dialogTitle", args.action.metaData["dialogTitle"]); - } - else { - appState.setMenuState("dialogTitle", args.action.name); - } - - var templateUrl; - var iframe; - - if (args.action.metaData["actionUrl"]) { - templateUrl = args.action.metaData["actionUrl"]; - iframe = true; - } - else if (args.action.metaData["actionView"]) { - templateUrl = args.action.metaData["actionView"]; - iframe = false; - } - else { - - //by convention we will look into the /views/{treetype}/{action}.html - // for example: /views/content/create.html - - //we will also check for a 'packageName' for the current tree, if it exists then the convention will be: - // for example: /App_Plugins/{mypackage}/backoffice/{treetype}/create.html - - var treeAlias = treeService.getTreeAlias(args.node); - var packageTreeFolder = treeService.getTreePackageFolder(treeAlias); - - if (!treeAlias) { - throw "Could not get tree alias for node " + args.node.id; - } - - if (packageTreeFolder) { - templateUrl = Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + - "/" + packageTreeFolder + - "/backoffice/" + treeAlias + "/" + args.action.alias + ".html"; - } - else { - templateUrl = "views/" + treeAlias + "/" + args.action.alias + ".html"; - } - - iframe = false; - } - - //TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with - // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog - // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, - // though would be v-easy, just not sure we want to ever support that? - - var dialog = dialogService.open( - { - container: $("#dialog div.umb-modalcolumn-body"), - //The ONLY reason we're passing in scope to the dialogService (which is legacy functionality) is - // for backwards compatibility since many dialogs require $scope.currentNode or $scope.currentAction - // to exist - scope: dialogScope, - inline: true, - show: true, - iframe: iframe, - modalClass: "umb-dialog", - template: templateUrl, - - //These will show up on the dialog controller's $scope under dialogOptions - currentNode: args.node, - currentAction: args.action, - }); - - //save the currently assigned dialog so it can be removed before a new one is created - currentDialog = dialog; - return dialog; - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideDialog - * @methodOf umbraco.services.navigationService - * - * @description - * hides the currently open dialog - */ - hideDialog: function (showMenu) { - - setMode("default"); - - if(showMenu){ - this.showMenu(undefined, { skipDefault: true, node: appState.getMenuState("currentNode") }); - } - }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#showSearch - * @methodOf umbraco.services.navigationService - * - * @description - * shows the search pane - */ - showSearch: function() { - setMode("search"); - }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideSearch - * @methodOf umbraco.services.navigationService - * - * @description - * hides the search pane + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#clearSelection + * @methodOf umbraco.services.listViewHelper + * + * @description + * Removes a given number of items and folders from the listviews selection array + * Folders can only be passed in if the listview is used in the media section which has a concept of folders. + * + * @param {Array} items Items to remove, can be null + * @param {Array} folders Folders to remove, can be null + * @param {Array} selection Listview selection, available as $scope.selection */ - hideSearch: function() { - setMode("default-hidesearch"); - }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideNavigation - * @methodOf umbraco.services.navigationService - * - * @description - * hides any open navigation panes and resets the tree, actions and the currently selected node - */ - hideNavigation: function() { - appState.setMenuState("menuActions", []); - setMode("default"); - } - }; - - return service; -} - -angular.module('umbraco.services').factory('navigationService', navigationService); - -/** - * @ngdoc service - * @name umbraco.services.notificationsService - * - * @requires $rootScope - * @requires $timeout - * @requires angularHelper - * - * @description - * Application-wide service for handling notifications, the umbraco application - * maintains a single collection of notications, which the UI watches for changes. - * By default when a notication is added, it is automaticly removed 7 seconds after - * This can be changed on add() - * - * ##usage - * To use, simply inject the notificationsService into any controller that needs it, and make - * sure the umbraco.services module is accesible - which it should be by default. - * - *
    - *		notificationsService.success("Document Published", "hooraaaay for you!");
    - *      notificationsService.error("Document Failed", "booooh");
    - * 
    - */ -angular.module('umbraco.services') -.factory('notificationsService', function ($rootScope, $timeout, angularHelper) { - - var nArray = []; - function setViewPath(view){ - if(view.indexOf('/') < 0) - { - view = "views/common/notifications/" + view; - } - - if(view.indexOf('.html') < 0) - { - view = view + ".html"; - } - return view; - } - - var service = { - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#add - * @methodOf umbraco.services.notificationsService - * - * @description - * Lower level api for adding notifcations, support more advanced options - * @param {Object} item The notification item - * @param {String} item.headline Short headline - * @param {String} item.message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @param {String} item.type Notification type, can be: "success","warning","error" or "info" - * @param {String} item.url url to open when notification is clicked - * @param {String} item.view path to custom view to load into the notification box - * @param {Array} item.actions Collection of button actions to append (label, func, cssClass) - * @param {Boolean} item.sticky if set to true, the notification will not auto-close - * @returns {Object} args notification object - */ - - add: function(item) { - angularHelper.safeApply($rootScope, function () { - - if(item.view){ - item.view = setViewPath(item.view); - item.sticky = true; - item.type = "form"; - item.headline = null; - } - - - //add a colon after the headline if there is a message as well - if (item.message) { - item.headline += ": "; - if(item.message.length > 200) { - item.sticky = true; - } - } - - //we need to ID the item, going by index isn't good enough because people can remove at different indexes - // whenever they want. Plus once we remove one, then the next index will be different. The only way to - // effectively remove an item is by an Id. - item.id = String.CreateGuid(); - - nArray.push(item); - - if(!item.sticky) { - $timeout(function() { - var found = _.find(nArray, function(i) { - return i.id === item.id; - }); - - if (found) { - var index = nArray.indexOf(found); - nArray.splice(index, 1); - } - - }, 7000); - } - - return item; - }); - - }, - - hasView : function(view){ - if(!view){ - return _.find(nArray, function(notification){ return notification.view;}); - }else{ - view = setViewPath(view).toLowerCase(); - return _.find(nArray, function(notification){ return notification.view.toLowerCase() === view;}); - } - }, - addView: function(view, args){ - var item = { - args: args, - view: view - }; - - service.add(item); - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#showNotification - * @methodOf umbraco.services.notificationsService - * - * @description - * Shows a notification based on the object passed in, normally used to render notifications sent back from the server - * - * @returns {Object} args notification object - */ - showNotification: function(args) { - if (!args) { - throw "args cannot be null"; - } - if (args.type === undefined || args.type === null) { - throw "args.type cannot be null"; - } - if (!args.header) { - throw "args.header cannot be null"; - } - - switch(args.type) { - case 0: - //save - this.success(args.header, args.message); - break; - case 1: - //info - this.success(args.header, args.message); - break; - case 2: - //error - this.error(args.header, args.message); - break; - case 3: - //success - this.success(args.header, args.message); - break; - case 4: - //warning - this.warning(args.header, args.message); - break; - } - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#success - * @methodOf umbraco.services.notificationsService - * - * @description - * Adds a green success notication to the notications collection - * This should be used when an operations *completes* without errors - * - * @param {String} headline Headline of the notification - * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @returns {Object} notification object - */ - success: function (headline, message) { - return service.add({ headline: headline, message: message, type: 'success', time: new Date() }); - }, - /** - * @ngdoc method - * @name umbraco.services.notificationsService#error - * @methodOf umbraco.services.notificationsService - * - * @description - * Adds a red error notication to the notications collection - * This should be used when an operations *fails* and could not complete - * - * @param {String} headline Headline of the notification - * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @returns {Object} notification object - */ - error: function (headline, message) { - return service.add({ headline: headline, message: message, type: 'error', time: new Date() }); - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#warning - * @methodOf umbraco.services.notificationsService - * - * @description - * Adds a yellow warning notication to the notications collection - * This should be used when an operations *completes* but something was not as expected - * - * - * @param {String} headline Headline of the notification - * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @returns {Object} notification object - */ - warning: function (headline, message) { - return service.add({ headline: headline, message: message, type: 'warning', time: new Date() }); - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#warning - * @methodOf umbraco.services.notificationsService - * - * @description - * Adds a yellow warning notication to the notications collection - * This should be used when an operations *completes* but something was not as expected - * - * - * @param {String} headline Headline of the notification - * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @returns {Object} notification object - */ - info: function (headline, message) { - return service.add({ headline: headline, message: message, type: 'info', time: new Date() }); - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#remove - * @methodOf umbraco.services.notificationsService - * - * @description - * Removes a notification from the notifcations collection at a given index - * - * @param {Int} index index where the notication should be removed from - */ - remove: function (index) { - if(angular.isObject(index)){ - var i = nArray.indexOf(index); - angularHelper.safeApply($rootScope, function() { - nArray.splice(i, 1); - }); - }else{ - angularHelper.safeApply($rootScope, function() { - nArray.splice(index, 1); - }); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#removeAll - * @methodOf umbraco.services.notificationsService - * - * @description - * Removes all notifications from the notifcations collection - */ - removeAll: function () { - angularHelper.safeApply($rootScope, function() { - nArray = []; - }); - }, - - /** - * @ngdoc property - * @name umbraco.services.notificationsService#current - * @propertyOf umbraco.services.notificationsService - * - * @description - * Returns an array of current notifications to display - * - * @returns {string} returns an array - */ - current: nArray, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#getCurrent - * @methodOf umbraco.services.notificationsService - * - * @description - * Method to return all notifications from the notifcations collection - */ - getCurrent: function(){ - return nArray; - } - }; - - return service; -}); -(function() { - 'use strict'; - - function overlayHelper() { - - var numberOfOverlays = 0; - - function registerOverlay() { - numberOfOverlays++; - return numberOfOverlays; - } - - function unregisterOverlay() { - numberOfOverlays--; - return numberOfOverlays; - } - - function getNumberOfOverlays() { - return numberOfOverlays; - } - - var service = { - numberOfOverlays: numberOfOverlays, - registerOverlay: registerOverlay, - unregisterOverlay: unregisterOverlay, - getNumberOfOverlays: getNumberOfOverlays - }; - - return service; - - } - - - angular.module('umbraco.services').factory('overlayHelper', overlayHelper); - - -})(); - -(function() { - 'use strict'; - - function platformService() { - - function isMac() { - return navigator.platform.toUpperCase().indexOf('MAC')>=0; - } - - //////////// - - var service = { - isMac: isMac - }; - - return service; - - } - - angular.module('umbraco.services').factory('platformService', platformService); - - -})(); - -/** - * @ngdoc service - * @name umbraco.services.searchService - * - * - * @description - * Service for handling the main application search, can currently search content, media and members - * - * ##usage - * To use, simply inject the searchService into any controller that needs it, and make - * sure the umbraco.services module is accesible - which it should be by default. - * - *
    - *      searchService.searchMembers({term: 'bob'}).then(function(results){
    - *          angular.forEach(results, function(result){
    - *                  //returns:
    - *                  {name: "name", id: 1234, menuUrl: "url", editorPath: "url", metaData: {}, subtitle: "/path/etc" }
    - *           })          
    - *           var result = 
    - *       }) 
    - * 
    - */ -angular.module('umbraco.services') -.factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper) { - - function configureMemberResult(member) { - member.menuUrl = umbRequestHelper.getApiUrl("memberTreeBaseUrl", "GetMenu", [{ id: member.id }, { application: 'member' }]); - member.editorPath = "member/member/edit/" + (member.key ? member.key : member.id); - angular.extend(member.metaData, { treeAlias: "member" }); - member.subTitle = member.metaData.Email; - } - - function configureMediaResult(media) - { - media.menuUrl = umbRequestHelper.getApiUrl("mediaTreeBaseUrl", "GetMenu", [{ id: media.id }, { application: 'media' }]); - media.editorPath = "media/media/edit/" + media.id; - angular.extend(media.metaData, { treeAlias: "media" }); - } - - function configureContentResult(content) { - content.menuUrl = umbRequestHelper.getApiUrl("contentTreeBaseUrl", "GetMenu", [{ id: content.id }, { application: 'content' }]); - content.editorPath = "content/content/edit/" + content.id; - angular.extend(content.metaData, { treeAlias: "content" }); - content.subTitle = content.metaData.Url; - } - - return { - - /** + function clearSelection(items, folders, selection) { + var i = 0; + selection.length = 0; + if (angular.isArray(items)) { + for (i = 0; items.length > i; i++) { + var item = items[i]; + item.selected = false; + } + } + if (angular.isArray(folders)) { + for (i = 0; folders.length > i; i++) { + var folder = folders[i]; + folder.selected = false; + } + } + } + /** * @ngdoc method - * @name umbraco.services.searchService#searchMembers - * @methodOf umbraco.services.searchService + * @name umbraco.services.listViewHelper#selectAllItems + * @methodOf umbraco.services.listViewHelper * * @description - * Searches the default member search index - * @param {Object} args argument object - * @param {String} args.term seach term - * @returns {Promise} returns promise containing all matching members - */ - searchMembers: function(args) { - - if (!args.term) { - throw "args.term is required"; - } - - return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) { - _.each(data, function(item) { - configureMemberResult(item); - }); - return data; - }); - }, - - /** + * Helper method for toggling the select state on all items in the active listview + * Can only be used from a checkbox as a checkbox $event is required to pass in. + * + * @param {Array} items Items to toggle selection on, should be $scope.items + * @param {Array} selection Listview selection, available as $scope.selection + * @param {$event} $event Event passed from the checkbox being toggled + */ + function selectAllItems(items, selection, $event) { + var checkbox = $event.target; + var clearSelection = false; + if (!angular.isArray(items)) { + return; + } + selection.length = 0; + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (checkbox.checked) { + selection.push({ + id: item.id, + key: item.key + }); + } else { + clearSelection = true; + } + item.selected = checkbox.checked; + } + if (clearSelection) { + selection.length = 0; + } + } + /** * @ngdoc method - * @name umbraco.services.searchService#searchContent - * @methodOf umbraco.services.searchService + * @name umbraco.services.listViewHelper#isSelectedAll + * @methodOf umbraco.services.listViewHelper * * @description - * Searches the default internal content search index - * @param {Object} args argument object - * @param {String} args.term seach term - * @returns {Promise} returns promise containing all matching content items - */ - searchContent: function(args) { - - if (!args.term) { - throw "args.term is required"; - } - - return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) { - _.each(data, function (item) { - configureContentResult(item); - }); - return data; - }); - }, - - /** + * Method to determine if all items on the current page in the list has been selected + * Given the current items in the view, and the current selection, it will return true/false + * + * @param {Array} items Items to test if all are selected, should be $scope.items + * @param {Array} selection Listview selection, available as $scope.selection + * @returns {Boolean} boolean indicate if all items in the listview have been selected + */ + function isSelectedAll(items, selection) { + var numberOfSelectedItem = 0; + for (var itemIndex = 0; items.length > itemIndex; itemIndex++) { + var item = items[itemIndex]; + for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) { + var selectedItem = selection[selectedIndex]; + // if item.id is 2147483647 (int.MaxValue) use item.key + if (item.id !== 2147483647 && item.id === selectedItem.id || item.key === selectedItem.key) { + numberOfSelectedItem++; + } + } + } + if (numberOfSelectedItem === items.length) { + return true; + } + } + /** * @ngdoc method - * @name umbraco.services.searchService#searchMedia - * @methodOf umbraco.services.searchService + * @name umbraco.services.listViewHelper#setSortingDirection + * @methodOf umbraco.services.listViewHelper * * @description - * Searches the default media search index - * @param {Object} args argument object - * @param {String} args.term seach term - * @returns {Promise} returns promise containing all matching media items - */ - searchMedia: function(args) { - - if (!args.term) { - throw "args.term is required"; - } - - return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) { - _.each(data, function (item) { - configureMediaResult(item); - }); - return data; - }); - }, - - /** + * *Internal* method for changing sort order icon + * @param {String} col Column alias to order after + * @param {String} direction Order direction `asc` or `desc` + * @param {Object} options object passed from the parent listview available as $scope.options + */ + function setSortingDirection(col, direction, options) { + return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction; + } + /** * @ngdoc method - * @name umbraco.services.searchService#searchAll - * @methodOf umbraco.services.searchService + * @name umbraco.services.listViewHelper#setSorting + * @methodOf umbraco.services.listViewHelper * * @description - * Searches all available indexes and returns all results in one collection - * @param {Object} args argument object - * @param {String} args.term seach term - * @returns {Promise} returns promise containing all matching items - */ - searchAll: function (args) { - - if (!args.term) { - throw "args.term is required"; - } - - return entityResource.searchAll(args.term, args.canceler).then(function (data) { - - _.each(data, function(resultByType) { - switch(resultByType.type) { - case "Document": - _.each(resultByType.results, function (item) { - configureContentResult(item); - }); - break; - case "Media": - _.each(resultByType.results, function (item) { - configureMediaResult(item); - }); - break; - case "Member": - _.each(resultByType.results, function (item) { - configureMemberResult(item); - }); - break; - } - }); - - return data; - }); - - }, - - //TODO: This doesn't do anything! - setCurrent: function(sectionAlias) { - - var currentSection = sectionAlias; - } - }; -}); -/** - * @ngdoc service - * @name umbraco.services.serverValidationManager - * @function - * + * Method for setting the field on which the listview will order its items after. + * + * @param {String} field Field alias to order after + * @param {Boolean} allow Determines if the user is allowed to set this field, normally true + * @param {Object} options Options object passed from the parent listview available as $scope.options + */ + function setSorting(field, allow, options) { + if (allow) { + if (options.orderBy === field && options.orderDirection === 'asc') { + options.orderDirection = 'desc'; + } else { + options.orderDirection = 'asc'; + } + options.orderBy = field; + } + } + //This takes in a dictionary of Ids with Permissions and determines + // the intersect of all permissions to return an object representing the + // listview button permissions + function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) { + if (currentIdsWithPermissions == null) { + currentIdsWithPermissions = {}; + } + //merge the newly retrieved permissions to the main dictionary + _.each(unmergedPermissions, function (value, key, list) { + currentIdsWithPermissions[key] = value; + }); + //get the intersect permissions + var arr = []; + _.each(currentIdsWithPermissions, function (value, key, list) { + arr.push(value); + }); + //we need to use 'apply' to call intersection with an array of arrays, + //see: http://stackoverflow.com/a/16229480/694494 + var intersectPermissions = _.intersection.apply(_, arr); + return { + canCopy: _.contains(intersectPermissions, 'O'), + //Magic Char = O + canCreate: _.contains(intersectPermissions, 'C'), + //Magic Char = C + canDelete: _.contains(intersectPermissions, 'D'), + //Magic Char = D + canMove: _.contains(intersectPermissions, 'M'), + //Magic Char = M + canPublish: _.contains(intersectPermissions, 'U'), + //Magic Char = U + canUnpublish: _.contains(intersectPermissions, 'U') + }; + } + var service = { + getLayout: getLayout, + getFirstAllowedLayout: getFirstAllowedLayout, + setLayout: setLayout, + saveLayoutInLocalStorage: saveLayoutInLocalStorage, + selectHandler: selectHandler, + selectItem: selectItem, + deselectItem: deselectItem, + clearSelection: clearSelection, + selectAllItems: selectAllItems, + isSelectedAll: isSelectedAll, + setSortingDirection: setSortingDirection, + setSorting: setSorting, + getButtonPermissions: getButtonPermissions + }; + return service; + } + angular.module('umbraco.services').factory('listViewHelper', listViewHelper); + }()); + /** + @ngdoc service + * @name umbraco.services.listViewPrevalueHelper + * + * * @description - * Used to handle server side validation and wires up the UI with the messages. There are 2 types of validation messages, one - * is for user defined properties (called Properties) and the other is for field properties which are attached to the native - * model objects (not user defined). The methods below are named according to these rules: Properties vs Fields. - */ -function serverValidationManager($timeout) { - - var callbacks = []; - - /** calls the callback specified with the errors specified, used internally */ - function executeCallback(self, errorsForCallback, callback) { - - callback.apply(self, [ - false, //pass in a value indicating it is invalid - errorsForCallback, //pass in the errors for this item - self.items]); //pass in all errors in total - } - - function getFieldErrors(self, fieldName) { - if (!angular.isString(fieldName)) { - throw "fieldName must be a string"; - } - - //find errors for this field name - return _.filter(self.items, function (item) { - return (item.propertyAlias === null && item.fieldName === fieldName); - }); - } - - function getPropertyErrors(self, propertyAlias, fieldName) { - if (!angular.isString(propertyAlias)) { - throw "propertyAlias must be a string"; - } - if (fieldName && !angular.isString(fieldName)) { - throw "fieldName must be a string"; - } - - //find all errors for this property - return _.filter(self.items, function (item) { - return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); - }); - } - - return { - - /** - * @ngdoc function - * @name umbraco.services.serverValidationManager#subscribe - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * This method needs to be called once all field and property errors are wired up. - * - * In some scenarios where the error collection needs to be persisted over a route change - * (i.e. when a content item (or any item) is created and the route redirects to the editor) - * the controller should call this method once the data is bound to the scope - * so that any persisted validation errors are re-bound to their controls. Once they are re-binded this then clears the validation - * colleciton so that if another route change occurs, the previously persisted validation errors are not re-bound to the new item. - */ - executeAndClearAllSubscriptions: function() { - - var self = this; - - $timeout(function () { - - for (var cb in callbacks) { - if (callbacks[cb].propertyAlias === null) { - //its a field error callback - var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName); - if (fieldErrors.length > 0) { - executeCallback(self, fieldErrors, callbacks[cb].callback); - } - } - else { - //its a property error - var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].fieldName); - if (propErrors.length > 0) { - executeCallback(self, propErrors, callbacks[cb].callback); - } - } - } - //now that they are all executed, we're gonna clear all of the errors we have - self.clear(); - }); - }, - - /** - * @ngdoc function - * @name umbraco.services.serverValidationManager#subscribe - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Adds a callback method that is executed whenever validation changes for the field name + property specified. - * This is generally used for server side validation in order to match up a server side validation error with - * a particular field, otherwise we can only pinpoint that there is an error for a content property, not the - * property's specific field. This is used with the val-server directive in which the directive specifies the - * field alias to listen for. - * If propertyAlias is null, then this subscription is for a field property (not a user defined property). - */ - subscribe: function (propertyAlias, fieldName, callback) { - if (!callback) { - return; - } - - if (propertyAlias === null) { - //don't add it if it already exists - var exists1 = _.find(callbacks, function (item) { - return item.propertyAlias === null && item.fieldName === fieldName; - }); - if (!exists1) { - callbacks.push({ propertyAlias: null, fieldName: fieldName, callback: callback }); - } - } - else if (propertyAlias !== undefined) { - //don't add it if it already exists - var exists2 = _.find(callbacks, function (item) { - return item.propertyAlias === propertyAlias && item.fieldName === fieldName; - }); - if (!exists2) { - callbacks.push({ propertyAlias: propertyAlias, fieldName: fieldName, callback: callback }); - } - } - }, - - unsubscribe: function (propertyAlias, fieldName) { - - if (propertyAlias === null) { - - //remove all callbacks for the content field - callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === null && item.fieldName === fieldName; - }); - - } - else if (propertyAlias !== undefined) { - - //remove all callbacks for the content property - callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === propertyAlias && - (item.fieldName === fieldName || - ((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === ""))); - }); - } - - - }, - - - /** - * @ngdoc function - * @name getPropertyCallbacks - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Gets all callbacks that has been registered using the subscribe method for the propertyAlias + fieldName combo. - * This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an - * explicit field name set. - */ - getPropertyCallbacks: function (propertyAlias, fieldName) { - var found = _.filter(callbacks, function (item) { - //returns any callback that have been registered directly against the field and for only the property - return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); - }); - return found; - }, - - /** - * @ngdoc function - * @name getFieldCallbacks - * @methodOf umbraco.services.serverValidationManager - * @function + * Service for accessing the prevalues of a list view being edited in the inline list view editor in the doctype editor + */ + (function () { + 'use strict'; + function listViewPrevalueHelper() { + var prevalues = []; + /** + * @ngdoc method + * @name umbraco.services.listViewPrevalueHelper#getPrevalues + * @methodOf umbraco.services.listViewPrevalueHelper + * + * @description + * Set the collection of prevalues + */ + function getPrevalues() { + return prevalues; + } + /** + * @ngdoc method + * @name umbraco.services.listViewPrevalueHelper#setPrevalues + * @methodOf umbraco.services.listViewPrevalueHelper + * + * @description + * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage + * + * @param {Array} values Array of prevalues + */ + function setPrevalues(values) { + prevalues = values; + } + var service = { + getPrevalues: getPrevalues, + setPrevalues: setPrevalues + }; + return service; + } + angular.module('umbraco.services').factory('listViewPrevalueHelper', listViewPrevalueHelper); + }()); + /** + * @ngdoc service + * @name umbraco.services.localizationService + * + * @requires $http + * @requires $q + * @requires $window + * @requires $filter + * + * @description + * Application-wide service for handling localization + * + * ##usage + * To use, simply inject the localizationService into any controller that needs it, and make + * sure the umbraco.services module is accesible - which it should be by default. + * + *
    + *    localizationService.localize("area_key").then(function(value){
    + *        element.html(value);
    + *    });
    + * 
    + */ + angular.module('umbraco.services').factory('localizationService', function ($http, $q, eventsService, $window, $filter, userService) { + //TODO: This should be injected as server vars + var url = 'LocalizedText'; + var resourceFileLoadStatus = 'none'; + var resourceLoadingPromise = []; + function _lookup(value, tokens, dictionary) { + //strip the key identifier if its there + if (value && value[0] === '@') { + value = value.substring(1); + } + //if no area specified, add general_ + if (value && value.indexOf('_') < 0) { + value = 'general_' + value; + } + var entry = dictionary[value]; + if (entry) { + if (tokens) { + for (var i = 0; i < tokens.length; i++) { + entry = entry.replace('%' + i + '%', tokens[i]); + } + } + return entry; + } + return '[' + value + ']'; + } + var service = { + // array to hold the localized resource string entries + dictionary: [], + // loads the language resource file from the server + initLocalizedResources: function () { + var deferred = $q.defer(); + if (resourceFileLoadStatus === 'loaded') { + deferred.resolve(service.dictionary); + return deferred.promise; + } + //if the resource is already loading, we don't want to force it to load another one in tandem, we'd rather + // wait for that initial http promise to finish and then return this one with the dictionary loaded + if (resourceFileLoadStatus === 'loading') { + //add to the list of promises waiting + resourceLoadingPromise.push(deferred); + //exit now it's already loading + return deferred.promise; + } + resourceFileLoadStatus = 'loading'; + // build the url to retrieve the localized resource file + $http({ + method: 'GET', + url: url, + cache: false + }).then(function (response) { + resourceFileLoadStatus = 'loaded'; + service.dictionary = response.data; + eventsService.emit('localizationService.updated', response.data); + deferred.resolve(response.data); + //ensure all other queued promises are resolved + for (var p in resourceLoadingPromise) { + resourceLoadingPromise[p].resolve(response.data); + } + }, function (err) { + deferred.reject('Something broke'); + //ensure all other queued promises are resolved + for (var p in resourceLoadingPromise) { + resourceLoadingPromise[p].reject('Something broke'); + } + }); + return deferred.promise; + }, + /** + * @ngdoc method + * @name umbraco.services.localizationService#tokenize + * @methodOf umbraco.services.localizationService * * @description - * Gets all callbacks that has been registered using the subscribe method for the field. - */ - getFieldCallbacks: function (fieldName) { - var found = _.filter(callbacks, function (item) { - //returns any callback that have been registered directly against the field - return (item.propertyAlias === null && item.fieldName === fieldName); - }); - return found; - }, - - /** - * @ngdoc function - * @name addFieldError - * @methodOf umbraco.services.serverValidationManager - * @function + * Helper to tokenize and compile a localization string + * @param {String} value the value to tokenize + * @param {Object} scope the $scope object + * @returns {String} tokenized resource string + */ + tokenize: function (value, scope) { + if (value) { + var localizer = value.split(':'); + var retval = { + tokens: undefined, + key: localizer[0].substring(0) + }; + if (localizer.length > 1) { + retval.tokens = localizer[1].split(','); + for (var x = 0; x < retval.tokens.length; x++) { + retval.tokens[x] = scope.$eval(retval.tokens[x]); + } + } + return retval; + } + return value; + }, + /** + * @ngdoc method + * @name umbraco.services.localizationService#localize + * @methodOf umbraco.services.localizationService * * @description - * Adds an error message for a native content item field (not a user defined property, for Example, 'Name') - */ - addFieldError: function(fieldName, errorMsg) { - if (!fieldName) { - return; - } - - //only add the item if it doesn't exist - if (!this.hasFieldError(fieldName)) { - this.items.push({ - propertyAlias: null, - fieldName: fieldName, - errorMsg: errorMsg - }); - } - - //find all errors for this item - var errorsForCallback = getFieldErrors(this, fieldName); - //we should now call all of the call backs registered for this error - var cbs = this.getFieldCallbacks(fieldName); - //call each callback for this error - for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback); - } - }, - - /** - * @ngdoc function - * @name addPropertyError - * @methodOf umbraco.services.serverValidationManager - * @function + * Checks the dictionary for a localized resource string + * @param {String} value the area/key to localize in the format of 'section_key' + * alternatively if no section is set such as 'key' then we assume the key is to be looked in + * the 'general' section + * + * @param {Array} tokens if specified this array will be sent as parameter values + * This replaces %0% and %1% etc in the dictionary key value with the passed in strings + * + * @returns {String} localized resource string + */ + localize: function (value, tokens) { + return service.initLocalizedResources().then(function (dic) { + var val = _lookup(value, tokens, dic); + return val; + }); + }, + /** + * @ngdoc method + * @name umbraco.services.localizationService#localizeMany + * @methodOf umbraco.services.localizationService * * @description - * Adds an error message for the content property - */ - addPropertyError: function (propertyAlias, fieldName, errorMsg) { - if (!propertyAlias) { - return; - } - - //only add the item if it doesn't exist - if (!this.hasPropertyError(propertyAlias, fieldName)) { - this.items.push({ - propertyAlias: propertyAlias, - fieldName: fieldName, - errorMsg: errorMsg - }); - } - - //find all errors for this item - var errorsForCallback = getPropertyErrors(this, propertyAlias, fieldName); - //we should now call all of the call backs registered for this error - var cbs = this.getPropertyCallbacks(propertyAlias, fieldName); - //call each callback for this error - for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback); - } - }, - - /** - * @ngdoc function - * @name removePropertyError - * @methodOf umbraco.services.serverValidationManager - * @function + * Checks the dictionary for multipe localized resource strings at once, preventing the need for nested promises + * with localizationService.localize + * + * ##Usage + *
    +         * localizationService.localizeMany(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){
    +         *      var header = data[0];
    +         *      var message = data[1];
    +         *      notificationService.error(header, message);
    +         * });
    +         * 
    + * + * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key' + * alternatively if no section is set such as 'key' then we assume the key is to be looked in + * the 'general' section + * + * @returns {Array} An array of localized resource string in the same order + */ + localizeMany: function (keys) { + if (keys) { + //The LocalizationService.localize promises we want to resolve + var promises = []; + for (var i = 0; i < keys.length; i++) { + promises.push(service.localize(keys[i], undefined)); + } + return $q.all(promises).then(function (localizedValues) { + return localizedValues; + }); + } + }, + /** + * @ngdoc method + * @name umbraco.services.localizationService#concat + * @methodOf umbraco.services.localizationService * * @description - * Removes an error message for the content property - */ - removePropertyError: function (propertyAlias, fieldName) { - - if (!propertyAlias) { - return; - } - //remove the item - this.items = _.reject(this.items, function (item) { - return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); - }); - }, - - /** - * @ngdoc function - * @name reset - * @methodOf umbraco.services.serverValidationManager - * @function + * Checks the dictionary for multipe localized resource strings at once & concats them to a single string + * Which was not possible with localizationSerivce.localize() due to returning a promise + * + * ##Usage + *
    +         * localizationService.concat(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){
    +         *      var combinedText = data;
    +         * });
    +         * 
    + * + * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key' + * alternatively if no section is set such as 'key' then we assume the key is to be looked in + * the 'general' section + * + * @returns {String} An concatenated string of localized resource string passed into the function in the same order + */ + concat: function (keys) { + if (keys) { + //The LocalizationService.localize promises we want to resolve + var promises = []; + for (var i = 0; i < keys.length; i++) { + promises.push(service.localize(keys[i], undefined)); + } + return $q.all(promises).then(function (localizedValues) { + //Build a concat string by looping over the array of resolved promises/translations + var returnValue = ''; + for (var i = 0; i < localizedValues.length; i++) { + returnValue += localizedValues[i]; + } + return returnValue; + }); + } + }, + /** + * @ngdoc method + * @name umbraco.services.localizationService#format + * @methodOf umbraco.services.localizationService * * @description - * Clears all errors and notifies all callbacks that all server errros are now valid - used when submitting a form - */ - reset: function () { - this.clear(); - for (var cb in callbacks) { - callbacks[cb].callback.apply(this, [ - true, //pass in a value indicating it is VALID - [], //pass in empty collection - []]); //pass in empty collection - } - }, - - /** - * @ngdoc function - * @name clear - * @methodOf umbraco.services.serverValidationManager - * @function + * Checks the dictionary for multipe localized resource strings at once & formats a tokenized message + * Which was not possible with localizationSerivce.localize() due to returning a promise + * + * ##Usage + *
    +         * localizationService.format(["template_insert", "template_insertSections"], "%0% %1%").then(function(data){
    +         *      //Will return 'Insert Sections'
    +         *      var formattedResult = data;
    +         * });
    +         * 
    + * + * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key' + * alternatively if no section is set such as 'key' then we assume the key is to be looked in + * the 'general' section + * + * @param {String} message is the string you wish to replace containing tokens in the format of %0% and %1% + * with the localized resource strings + * + * @returns {String} An concatenated string of localized resource string passed into the function in the same order + */ + format: function (keys, message) { + if (keys) { + //The LocalizationService.localize promises we want to resolve + var promises = []; + for (var i = 0; i < keys.length; i++) { + promises.push(service.localize(keys[i], undefined)); + } + return $q.all(promises).then(function (localizedValues) { + //Replace {0} and {1} etc in message with the localized values + for (var i = 0; i < localizedValues.length; i++) { + var token = '%' + i + '%'; + var regex = new RegExp(token, 'g'); + message = message.replace(regex, localizedValues[i]); + } + return message; + }); + } + } + }; + //This happens after login / auth and assets loading + eventsService.on('app.authenticated', function () { + resourceFileLoadStatus = 'none'; + resourceLoadingPromise = []; + }); + // return the local instance when called + return service; + }); + /** + * @ngdoc service + * @name umbraco.services.macroService + * + * + * @description + * A service to return macro information such as generating syntax to insert a macro into an editor + */ + function macroService() { + return { + /** parses the special macro syntax like and returns an object with the macro alias and it's parameters */ + parseMacroSyntax: function (syntax) { + //This regex will match an alias of anything except characters that are quotes or new lines (for legacy reasons, when new macros are created + // their aliases are cleaned an invalid chars are stripped) + var expression = /(<\?UMBRACO_MACRO (?:.+?)?macroAlias=["']([^\"\'\n\r]+?)["'][\s\S]+?)(\/>|>.*?<\/\?UMBRACO_MACRO>)/i; + var match = expression.exec(syntax); + if (!match || match.length < 3) { + return null; + } + var alias = match[2]; + //this will leave us with just the parameters + var paramsChunk = match[1].trim().replace(new RegExp('UMBRACO_MACRO macroAlias=["\']' + alias + '["\']'), '').trim(); + var paramExpression = /(\w+?)=['\"]([\s\S]*?)['\"]/g; + var paramMatch; + var returnVal = { + macroAlias: alias, + macroParamsDictionary: {} + }; + while (paramMatch = paramExpression.exec(paramsChunk)) { + returnVal.macroParamsDictionary[paramMatch[1]] = paramMatch[2]; + } + return returnVal; + }, + /** + * @ngdoc function + * @name umbraco.services.macroService#generateWebFormsSyntax + * @methodOf umbraco.services.macroService + * @function * * @description - * Clears all errors - */ - clear: function() { - this.items = []; - }, - - /** + * generates the syntax for inserting a macro into a rich text editor - this is the very old umbraco style syntax + * + * @param {object} args an object containing the macro alias and it's parameter values + */ + generateMacroSyntax: function (args) { + // + var macroString = ''; + return macroString; + }, + /** * @ngdoc function - * @name getPropertyError - * @methodOf umbraco.services.serverValidationManager - * @function + * @name umbraco.services.macroService#generateWebFormsSyntax + * @methodOf umbraco.services.macroService + * @function * * @description - * Gets the error message for the content property - */ - getPropertyError: function (propertyAlias, fieldName) { - var err = _.find(this.items, function (item) { - //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); - }); - return err; - }, - - /** + * generates the syntax for inserting a macro into a webforms templates + * + * @param {object} args an object containing the macro alias and it's parameter values + */ + generateWebFormsSyntax: function (args) { + var macroString = ''; + return macroString; + }, + /** * @ngdoc function - * @name getFieldError - * @methodOf umbraco.services.serverValidationManager - * @function + * @name umbraco.services.macroService#generateMvcSyntax + * @methodOf umbraco.services.macroService + * @function * * @description - * Gets the error message for a content field - */ - getFieldError: function (fieldName) { - var err = _.find(this.items, function (item) { - //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === null && item.fieldName === fieldName); - }); - return err; - }, - - /** + * generates the syntax for inserting a macro into an mvc template + * + * @param {object} args an object containing the macro alias and it's parameter values + */ + generateMvcSyntax: function (args) { + var macroString = '@Umbraco.RenderMacro("' + args.macroAlias + '"'; + var hasParams = false; + var paramString; + if (args.macroParamsDictionary) { + paramString = ', new {'; + _.each(args.macroParamsDictionary, function (val, key) { + hasParams = true; + var keyVal = key + '="' + (val ? val : '') + '", '; + paramString += keyVal; + }); + //remove the last , + paramString = paramString.trimEnd(', '); + paramString += '}'; + } + if (hasParams) { + macroString += paramString; + } + macroString += ')'; + return macroString; + }, + collectValueData: function (macro, macroParams, renderingEngine) { + var paramDictionary = {}; + var macroAlias = macro.alias; + var syntax; + _.each(macroParams, function (item) { + var val = item.value; + if (item.value !== null && item.value !== undefined && !_.isString(item.value)) { + try { + val = angular.toJson(val); + } catch (e) { + } + } + //each value needs to be xml escaped!! since the value get's stored as an xml attribute + paramDictionary[item.alias] = _.escape(val); + }); + //get the syntax based on the rendering engine + if (renderingEngine && renderingEngine === 'WebForms') { + syntax = this.generateWebFormsSyntax({ + macroAlias: macroAlias, + macroParamsDictionary: paramDictionary + }); + } else if (renderingEngine && renderingEngine === 'Mvc') { + syntax = this.generateMvcSyntax({ + macroAlias: macroAlias, + macroParamsDictionary: paramDictionary + }); + } else { + syntax = this.generateMacroSyntax({ + macroAlias: macroAlias, + macroParamsDictionary: paramDictionary + }); + } + var macroObject = { + 'macroParamsDictionary': paramDictionary, + 'macroAlias': macroAlias, + 'syntax': syntax + }; + return macroObject; + } + }; + } + angular.module('umbraco.services').factory('macroService', macroService); + /** +* @ngdoc service +* @name umbraco.services.mediaHelper +* @description A helper object used for dealing with media items +**/ + function mediaHelper(umbRequestHelper) { + //container of fileresolvers + var _mediaFileResolvers = {}; + return { + /** * @ngdoc function - * @name hasPropertyError - * @methodOf umbraco.services.serverValidationManager - * @function + * @name umbraco.services.mediaHelper#getImagePropertyValue + * @methodOf umbraco.services.mediaHelper + * @function * * @description - * Checks if the content property + field name combo has an error - */ - hasPropertyError: function (propertyAlias, fieldName) { - var err = _.find(this.items, function (item) { - //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); - }); - return err ? true : false; - }, - - /** + * Returns the file path associated with the media property if there is one + * + * @param {object} options Options object + * @param {object} options.mediaModel The media object to retrieve the image path from + * @param {object} options.imageOnly Optional, if true then will only return a path if the media item is an image + */ + getMediaPropertyValue: function (options) { + if (!options || !options.mediaModel) { + throw 'The options objet does not contain the required parameters: mediaModel'; + } + //combine all props, TODO: we really need a better way then this + var props = []; + if (options.mediaModel.properties) { + props = options.mediaModel.properties; + } else { + $(options.mediaModel.tabs).each(function (i, tab) { + props = props.concat(tab.properties); + }); + } + var mediaRoot = Umbraco.Sys.ServerVariables.umbracoSettings.mediaPath; + var imageProp = _.find(props, function (item) { + if (item.alias === 'umbracoFile') { + return true; + } + //this performs a simple check to see if we have a media file as value + //it doesnt catch everything, but better then nothing + if (angular.isString(item.value) && item.value.indexOf(mediaRoot) === 0) { + return true; + } + return false; + }); + if (!imageProp) { + return ''; + } + var mediaVal; + //our default images might store one or many images (as csv) + var split = imageProp.value.split(','); + var self = this; + mediaVal = _.map(split, function (item) { + return { + file: item, + isImage: self.detectIfImageByExtension(item) + }; + }); + //for now we'll just return the first image in the collection. + //TODO: we should enable returning many to be displayed in the picker if the uploader supports many. + if (mediaVal.length && mediaVal.length > 0) { + if (!options.imageOnly || options.imageOnly === true && mediaVal[0].isImage) { + return mediaVal[0].file; + } + } + return ''; + }, + /** * @ngdoc function - * @name hasFieldError - * @methodOf umbraco.services.serverValidationManager - * @function + * @name umbraco.services.mediaHelper#getImagePropertyValue + * @methodOf umbraco.services.mediaHelper + * @function * * @description - * Checks if a content field has an error - */ - hasFieldError: function (fieldName) { - var err = _.find(this.items, function (item) { - //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === null && item.fieldName === fieldName); - }); - return err ? true : false; - }, - - /** The array of error messages */ - items: [] - }; -} - -angular.module('umbraco.services').factory('serverValidationManager', serverValidationManager); -(function() { - 'use strict'; - - function templateHelperService(localizationService) { - - //crappy hack due to dictionary items not in umbracoNode table - function getInsertDictionarySnippet(nodeName) { - return "@Umbraco.GetDictionaryValue(\"" + nodeName + "\")"; - } - - function getInsertPartialSnippet(parentId, nodeName) { - - var partialViewName = nodeName.replace(".cshtml", ""); - - if(parentId) { - partialViewName = parentId + "/" + partialViewName; - } - - return "@Html.Partial(\"" + partialViewName + "\")"; - } - - function getQuerySnippet(queryExpression) { - var code = "\n@{\n" + "\tvar selection = " + queryExpression + ";\n}\n"; - code += "
      \n" + - "\t@foreach(var item in selection){\n" + - "\t\t
    • \n" + - "\t\t\t@item.Name\n" + - "\t\t
    • \n" + - "\t}\n" + - "
    \n\n"; - return code; - } - - function getRenderBodySnippet() { - return "@RenderBody()"; - } - - function getRenderSectionSnippet(sectionName, mandatory) { - return "@RenderSection(\"" + sectionName + "\", " + mandatory + ")"; - } - - function getAddSectionSnippet(sectionName) { - return "@section " + sectionName + "\r\n{\r\n\r\n\t{0}\r\n\r\n}\r\n"; - } - - function getGeneralShortcuts(){ - return { - "name": localizationService.localize("shortcuts_generalHeader"), - "shortcuts": [ - { - "description": localizationService.localize("buttons_undo"), - "keys": [{ "key": "ctrl" }, { "key": "z" }] - }, - { - "description": localizationService.localize("buttons_redo"), - "keys": [{ "key": "ctrl" }, { "key": "y" }] - }, - { - "description": localizationService.localize("buttons_save"), - "keys": [{ "key": "ctrl" }, { "key": "s" }] + * Returns the actual image path associated with the image property if there is one + * + * @param {object} options Options object + * @param {object} options.imageModel The media object to retrieve the image path from + */ + getImagePropertyValue: function (options) { + if (!options || !options.imageModel && !options.mediaModel) { + throw 'The options objet does not contain the required parameters: imageModel'; + } + //required to support backwards compatibility. + options.mediaModel = options.imageModel ? options.imageModel : options.mediaModel; + options.imageOnly = true; + return this.getMediaPropertyValue(options); + }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getThumbnail + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * formats the display model used to display the content to the model used to save the content + * + * @param {object} options Options object + * @param {object} options.imageModel The media object to retrieve the image path from + */ + getThumbnail: function (options) { + if (!options || !options.imageModel) { + throw 'The options objet does not contain the required parameters: imageModel'; + } + var imagePropVal = this.getImagePropertyValue(options); + if (imagePropVal !== '') { + return this.getThumbnailFromPath(imagePropVal); + } + return ''; + }, + registerFileResolver: function (propertyEditorAlias, func) { + _mediaFileResolvers[propertyEditorAlias] = func; + }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#resolveFileFromEntity + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Gets the media file url for a media entity returned with the entityResource + * + * @param {object} mediaEntity A media Entity returned from the entityResource + * @param {boolean} thumbnail Whether to return the thumbnail url or normal url + */ + resolveFileFromEntity: function (mediaEntity, thumbnail) { + if (!angular.isObject(mediaEntity.metaData)) { + throw 'Cannot resolve the file url from the mediaEntity, it does not contain the required metaData'; + } + var values = _.values(mediaEntity.metaData); + for (var i = 0; i < values.length; i++) { + var val = values[i]; + if (angular.isObject(val) && val.PropertyEditorAlias) { + for (var resolver in _mediaFileResolvers) { + if (val.PropertyEditorAlias === resolver) { + //we need to format a property variable that coincides with how the property would be structured + // if it came from the mediaResource just to keep things slightly easier for the file resolvers. + var property = { value: val.Value }; + return _mediaFileResolvers[resolver](property, mediaEntity, thumbnail); + } } - ] - }; - } - - function getEditorShortcuts(){ - return { - "name": localizationService.localize("shortcuts_editorHeader"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_commentLine"), - "keys": [{ "key": "ctrl" }, { "key": "/" }] - }, - { - "description": localizationService.localize("shortcuts_removeLine"), - "keys": [{ "key": "ctrl" }, { "key": "d" }] - }, - { - "description": localizationService.localize("shortcuts_copyLineUp"), - "keys": { - "win": [{ "key": "alt" }, { "key": "shift" }, { "key": "up" }], - "mac": [{ "key": "cmd" }, { "key": "alt" }, { "key": "up" }] + } + } + return ''; + }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#resolveFile + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Gets the media file url for a media object returned with the mediaResource + * + * @param {object} mediaEntity A media Entity returned from the entityResource + * @param {boolean} thumbnail Whether to return the thumbnail url or normal url + */ + /*jshint loopfunc: true */ + resolveFile: function (mediaItem, thumbnail) { + function iterateProps(props) { + var res = null; + for (var resolver in _mediaFileResolvers) { + var property = _.find(props, function (prop) { + return prop.editor === resolver; + }); + if (property) { + res = _mediaFileResolvers[resolver](property, mediaItem, thumbnail); + break; + } + } + return res; + } + //we either have properties raw on the object, or spread out on tabs + var result = ''; + if (mediaItem.properties) { + result = iterateProps(mediaItem.properties); + } else if (mediaItem.tabs) { + for (var tab in mediaItem.tabs) { + if (mediaItem.tabs[tab].properties) { + result = iterateProps(mediaItem.tabs[tab].properties); + if (result) { + break; } - }, - { - "description": localizationService.localize("shortcuts_copyLineDown"), - "keys": { - "win": [{ "key": "alt" }, { "key": "shift" }, { "key": "down" }], - "mac": [{ "key": "cmd" }, { "key": "alt" }, { "key": "down" }] - } - }, - { - "description": localizationService.localize("shortcuts_moveLineUp"), - "keys": [{ "key": "alt" }, { "key": "up" }] - }, - { - "description": localizationService.localize("shortcuts_moveLineDown"), - "keys": [{ "key": "alt" }, { "key": "down" }] } - ] - }; - } - - function getTemplateEditorShortcuts(){ - return { - "name": "Umbraco", //No need to localise Umbraco is the same in all languages :) - "shortcuts": [ - { - "description": localizationService.format(["template_insert", "template_insertPageField"], "%0% %1%"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "v" }] - }, - { - "description": localizationService.format(["template_insert", "template_insertPartialView"], "%0% %1%"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] - }, - { - "description": localizationService.format(["template_insert", "template_insertDictionaryItem"], "%0% %1%"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] - }, - { - "description": localizationService.format(["template_insert", "template_insertMacro"], "%0% %1%"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "m" }] - }, - { - "description": localizationService.localize("template_queryBuilder"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "q" }] - }, - { - "description": localizationService.format(["template_insert", "template_insertSections"], "%0% %1%"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "s" }] - }, - { - "description": localizationService.localize("template_mastertemplate"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] + } + } + return result; + }, + /*jshint loopfunc: true */ + hasFilePropertyType: function (mediaItem) { + function iterateProps(props) { + var res = false; + for (var resolver in _mediaFileResolvers) { + var property = _.find(props, function (prop) { + return prop.editor === resolver; + }); + if (property) { + res = true; + break; } - ] - }; - } - - function getPartialViewEditorShortcuts(){ - return { - "name": "Umbraco", //No need to localise Umbraco is the same in all languages :) - "shortcuts": [ - { - "description": localizationService.format(["template_insert", "template_insertPageField"], "%0% %1%"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "v" }] - }, - { - "description": localizationService.format(["template_insert", "template_insertDictionaryItem"], "%0% %1%"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] - }, - { - "description": localizationService.format(["template_insert", "template_insertMacro"], "%0% %1%"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "m" }] - }, - { - "description": localizationService.localize("template_queryBuilder"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "q" }] + } + return res; + } + //we either have properties raw on the object, or spread out on tabs + var result = false; + if (mediaItem.properties) { + result = iterateProps(mediaItem.properties); + } else if (mediaItem.tabs) { + for (var tab in mediaItem.tabs) { + if (mediaItem.tabs[tab].properties) { + result = iterateProps(mediaItem.tabs[tab].properties); + if (result) { + break; + } } - ] - }; - } - - //////////// - - var service = { - getInsertDictionarySnippet: getInsertDictionarySnippet, - getInsertPartialSnippet: getInsertPartialSnippet, - getQuerySnippet: getQuerySnippet, - getRenderBodySnippet: getRenderBodySnippet, - getRenderSectionSnippet: getRenderSectionSnippet, - getAddSectionSnippet: getAddSectionSnippet, - getGeneralShortcuts: getGeneralShortcuts, - getEditorShortcuts: getEditorShortcuts, - getTemplateEditorShortcuts: getTemplateEditorShortcuts, - getPartialViewEditorShortcuts: getPartialViewEditorShortcuts - }; - - return service; - - } - - angular.module('umbraco.services').factory('templateHelper', templateHelperService); - - -})(); - -/** - * @ngdoc service - * @name umbraco.services.tinyMceService - * - * - * @description - * A service containing all logic for all of the Umbraco TinyMCE plugins - */ -function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) { - return { - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#configuration - * @methodOf umbraco.services.tinyMceService - * - * @description - * Returns a collection of plugins available to the tinyMCE editor - * - */ - configuration: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "rteApiBaseUrl", - "GetConfiguration"), { cache: true }), - 'Failed to retrieve tinymce configuration'); - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#defaultPrevalues - * @methodOf umbraco.services.tinyMceService - * - * @description - * Returns a default configration to fallback on in case none is provided - * - */ - defaultPrevalues: function () { - var cfg = {}; - cfg.toolbar = ["code", "bold", "italic", "styleselect","alignleft", "aligncenter", "alignright", "bullist","numlist", "outdent", "indent", "link", "image", "umbmediapicker", "umbembeddialog", "umbmacro"]; - cfg.stylesheets = []; - cfg.dimensions = { height: 500 }; - cfg.maxImageSize = 500; - return cfg; - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia - * @methodOf umbraco.services.tinyMceService - * - * @description - * Creates the umbrco insert embedded media tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - * @param {Object} $scope the current controller scope - */ - createInsertEmbeddedMedia: function (editor, scope, callback) { - editor.addButton('umbembeddialog', { - icon: 'custom icon-tv', - tooltip: 'Embed', - onclick: function () { - if (callback) { - callback(); - } - } - }); - }, - - insertEmbeddedMediaInEditor: function(editor, preview) { - editor.insertContent(preview); - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#createMediaPicker - * @methodOf umbraco.services.tinyMceService - * - * @description - * Creates the umbrco insert media tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - * @param {Object} $scope the current controller scope - */ - createMediaPicker: function (editor, scope, callback) { - editor.addButton('umbmediapicker', { - icon: 'custom icon-picture', - tooltip: 'Media Picker', - onclick: function () { - - var selectedElm = editor.selection.getNode(), - currentTarget; - - - if(selectedElm.nodeName === 'IMG'){ - var img = $(selectedElm); - - var hasUdi = img.attr("data-udi") ? true : false; - - currentTarget = { - altText: img.attr("alt"), - url: img.attr("src") - }; - - if (hasUdi) { - currentTarget["udi"] = img.attr("data-udi"); - } - else { - currentTarget["id"] = img.attr("rel"); - } - } - - userService.getCurrentUser().then(function(userData) { - if(callback) { - callback(currentTarget, userData); - } - }); - - } - }); - }, - - insertMediaInEditor: function(editor, img) { - if(img) { - - var hasUdi = img.udi ? true : false; - - var data = { - alt: img.altText || "", - src: (img.url) ? img.url : "nothing.jpg", - id: '__mcenew' - }; - - if (hasUdi) { - data["data-udi"] = img.udi; - } - else { - //Considering these fixed because UDI will now be used and thus - // we have no need for rel http://issues.umbraco.org/issue/U4-6228, http://issues.umbraco.org/issue/U4-6595 - data["rel"] = img.id; - data["data-id"] = img.id; - } - - editor.insertContent(editor.dom.createHTML('img', data)); - - $timeout(function () { - var imgElm = editor.dom.get('__mcenew'); - var size = editor.dom.getSize(imgElm); - - if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { - var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); - - var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;"; - editor.dom.setAttrib(imgElm, 'style', s); - editor.dom.setAttrib(imgElm, 'id', null); - - if (img.url) { - var src = img.url + "?width=" + newSize.width + "&height=" + newSize.height; - editor.dom.setAttrib(imgElm, 'data-mce-src', src); - } - } - }, 500); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#createUmbracoMacro - * @methodOf umbraco.services.tinyMceService - * - * @description - * Creates the insert umbrco macro tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - * @param {Object} $scope the current controller scope - */ - createInsertMacro: function (editor, $scope, callback) { - - var createInsertMacroScope = this; - - /** Adds custom rules for the macro plugin and custom serialization */ - editor.on('preInit', function (args) { - //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out - editor.serializer.addRules('div'); - - /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ - editor.serializer.addNodeFilter('div', function (nodes, name) { - for (var i = 0; i < nodes.length; i++) { - if (nodes[i].attr("class") === "umb-macro-holder" && nodes[i].parent && nodes[i].parent.name.toUpperCase() === "P") { - nodes[i].parent.unwrap(); - } - } - }); - - }); - + } + } + return result; + }, /** - * Because the macro gets wrapped in a P tag because of the way 'enter' works, this - * method will return the macro element if not wrapped in a p, or the p if the macro - * element is the only one inside of it even if we are deep inside an element inside the macro - */ - function getRealMacroElem(element) { - var e = $(element).closest(".umb-macro-holder"); - if (e.length > 0) { - if (e.get(0).parentNode.nodeName === "P") { - //now check if we're the only element - if (element.parentNode.childNodes.length === 1) { - return e.get(0).parentNode; - } - } - return e.get(0); - } - return null; - } - - /** Adds the button instance */ - editor.addButton('umbmacro', { - icon: 'custom icon-settings-alt', - tooltip: 'Insert macro', - onPostRender: function () { - - var ctrl = this; - var isOnMacroElement = false; - - /** - if the selection comes from a different element that is not the macro's - we need to check if the selection includes part of the macro, if so we'll force the selection - to clear to the next element since if people can select part of the macro markup they can then modify it. - */ - function handleSelectionChange() { - - if (!editor.selection.isCollapsed()) { - var endSelection = tinymce.activeEditor.selection.getEnd(); - var startSelection = tinymce.activeEditor.selection.getStart(); - //don't proceed if it's an entire element selected - if (endSelection !== startSelection) { - - //if the end selection is a macro then move the cursor - //NOTE: we don't have to handle when the selection comes from a previous parent because - // that is automatically taken care of with the normal onNodeChanged logic since the - // evt.element will be the macro once it becomes part of the selection. - var $testForMacro = $(endSelection).closest(".umb-macro-holder"); - if ($testForMacro.length > 0) { - - //it came from before so move after, if there is no after then select ourselves - var next = $testForMacro.next(); - if (next.length > 0) { - editor.selection.setCursorLocation($testForMacro.next().get(0)); - } - else { - selectMacroElement($testForMacro.get(0)); - } - - } - } - } - } - - /** helper method to select the macro element */ - function selectMacroElement(macroElement) { - - // move selection to top element to ensure we can't edit this - editor.selection.select(macroElement); - - // check if the current selection *is* the element (ie bug) - var currentSelection = editor.selection.getStart(); - if (tinymce.isIE) { - if (!editor.dom.hasClass(currentSelection, 'umb-macro-holder')) { - while (!editor.dom.hasClass(currentSelection, 'umb-macro-holder') && currentSelection.parentNode) { - currentSelection = currentSelection.parentNode; - } - editor.selection.select(currentSelection); - } - } - } - - /** - * Add a node change handler, test if we're editing a macro and select the whole thing, then set our isOnMacroElement flag. - * If we change the selection inside this method, then we end up in an infinite loop, so we have to remove ourselves - * from the event listener before changing selection, however, it seems that putting a break point in this method - * will always cause an 'infinite' loop as the caret keeps changing. - */ - function onNodeChanged(evt) { - - //set our macro button active when on a node of class umb-macro-holder - var $macroElement = $(evt.element).closest(".umb-macro-holder"); - - handleSelectionChange(); - - //set the button active - ctrl.active($macroElement.length !== 0); - - if ($macroElement.length > 0) { - var macroElement = $macroElement.get(0); - - //remove the event listener before re-selecting - editor.off('NodeChange', onNodeChanged); - - selectMacroElement(macroElement); - - //set the flag - isOnMacroElement = true; - - //re-add the event listener - editor.on('NodeChange', onNodeChanged); - } - else { - isOnMacroElement = false; - } - - } - - /** when the contents load we need to find any macros declared and load in their content */ - editor.on("LoadContent", function (o) { - - //get all macro divs and load their content - $(editor.dom.select(".umb-macro-holder.mceNonEditable")).each(function() { - createInsertMacroScope.loadMacroContent($(this), null, $scope); - }); - - }); - - /** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */ - editor.on('BeforeExecCommand', function (o) { - if (isOnMacroElement) { - if (o.preventDefault) { - o.preventDefault(); - } - if (o.stopImmediatePropagation) { - o.stopImmediatePropagation(); - } - return; - } - }); - - /** This double checks and ensures you can't paste content into the rendered macro */ - editor.on("Paste", function (o) { - if (isOnMacroElement) { - if (o.preventDefault) { - o.preventDefault(); - } - if (o.stopImmediatePropagation) { - o.stopImmediatePropagation(); - } - return; - } - }); - - //set onNodeChanged event listener - editor.on('NodeChange', onNodeChanged); - - /** - * Listen for the keydown in the editor, we'll check if we are currently on a macro element, if so - * we'll check if the key down is a supported key which requires an action, otherwise we ignore the request - * so the macro cannot be edited. - */ - editor.on('KeyDown', function (e) { - if (isOnMacroElement) { - var macroElement = editor.selection.getNode(); - - //get the 'real' element (either p or the real one) - macroElement = getRealMacroElem(macroElement); - - //prevent editing - e.preventDefault(); - e.stopPropagation(); - - var moveSibling = function (element, isNext) { - var $e = $(element); - var $sibling = isNext ? $e.next() : $e.prev(); - if ($sibling.length > 0) { - editor.selection.select($sibling.get(0)); - editor.selection.collapse(true); - } - else { - //if we're moving previous and there is no sibling, then lets recurse and just select the next one - if (!isNext) { - moveSibling(element, true); - return; - } - - //if there is no sibling we'll generate a new p at the end and select it - editor.setContent(editor.getContent() + "

     

    "); - editor.selection.select($(editor.dom.getRoot()).children().last().get(0)); - editor.selection.collapse(true); - - } - }; - - //supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left) - //supported keys to remove the macro (8-backspace, 46-delete) - //TODO: Should we make the enter key insert a line break before or leave it as moving to the next element? - if ($.inArray(e.keyCode, [13, 40, 39]) !== -1) { - //move to next element - moveSibling(macroElement, true); - } - else if ($.inArray(e.keyCode, [27, 38, 37]) !== -1) { - //move to prev element - moveSibling(macroElement, false); - } - else if ($.inArray(e.keyCode, [8, 46]) !== -1) { - //delete macro element - - //move first, then delete - moveSibling(macroElement, false); - editor.dom.remove(macroElement); - } - return ; - } - }); - - }, - - /** The insert macro button click event handler */ - onclick: function () { - - var dialogData = { - //flag for use in rte so we only show macros flagged for the editor - richTextEditor: true - }; - - //when we click we could have a macro already selected and in that case we'll want to edit the current parameters - //so we'll need to extract them and submit them to the dialog. - var macroElement = editor.selection.getNode(); - macroElement = getRealMacroElem(macroElement); - if (macroElement) { - //we have a macro selected so we'll need to parse it's alias and parameters - var contents = $(macroElement).contents(); - var comment = _.find(contents, function(item) { - return item.nodeType === 8; - }); - if (!comment) { - throw "Cannot parse the current macro, the syntax in the editor is invalid"; - } - var syntax = comment.textContent.trim(); - var parsed = macroService.parseMacroSyntax(syntax); - dialogData = { - macroData: parsed - }; - } - - if(callback) { - callback(dialogData); - } - - } - }); - }, - - insertMacroInEditor: function(editor, macroObject, $scope) { - - //put the macro syntax in comments, we will parse this out on the server side to be used - //for persisting. - var macroSyntaxComment = ""; - //create an id class for this element so we can re-select it after inserting - var uniqueId = "umb-macro-" + editor.dom.uniqueId(); - var macroDiv = editor.dom.create('div', - { - 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId - }, - macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); - - editor.selection.setNode(macroDiv); - - var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); - - //async load the macro content - this.loadMacroContent($macroDiv, macroObject, $scope); - - }, - - /** loads in the macro content async from the server */ - loadMacroContent: function($macroDiv, macroData, $scope) { - - //if we don't have the macroData, then we'll need to parse it from the macro div - if (!macroData) { - var contents = $macroDiv.contents(); - var comment = _.find(contents, function (item) { - return item.nodeType === 8; - }); - if (!comment) { - throw "Cannot parse the current macro, the syntax in the editor is invalid"; - } - var syntax = comment.textContent.trim(); - var parsed = macroService.parseMacroSyntax(syntax); - macroData = parsed; - } - - var $ins = $macroDiv.find("ins"); - - //show the throbber - $macroDiv.addClass("loading"); - - var contentId = $routeParams.id; - - //need to wrap in safe apply since this might be occuring outside of angular - angularHelper.safeApply($scope, function() { - macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) - .then(function (htmlResult) { - - $macroDiv.removeClass("loading"); - htmlResult = htmlResult.trim(); - if (htmlResult !== "") { - $ins.html(htmlResult); - } - }); - }); - - }, - - createLinkPicker: function(editor, $scope, onClick) { - - function createLinkList(callback) { - return function() { - var linkList = editor.settings.link_list; - - if (typeof(linkList) === "string") { - tinymce.util.XHR.send({ - url: linkList, - success: function(text) { - callback(tinymce.util.JSON.parse(text)); - } - }); - } else { - callback(linkList); - } - }; - } - - function showDialog(linkList) { - var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText; - var win, linkListCtrl, relListCtrl, targetListCtrl; - - function linkListChangeHandler(e) { - var textCtrl = win.find('#text'); - - if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { - textCtrl.value(e.control.text()); - } - - win.find('#href').value(e.control.value()); - } - - function buildLinkList() { - var linkListItems = [{ - text: 'None', - value: '' - }]; - - tinymce.each(linkList, function(link) { - linkListItems.push({ - text: link.text || link.title, - value: link.value || link.url, - menu: link.menu - }); - }); - - return linkListItems; - } - - function buildRelList(relValue) { - var relListItems = [{ - text: 'None', - value: '' - }]; - - tinymce.each(editor.settings.rel_list, function(rel) { - relListItems.push({ - text: rel.text || rel.title, - value: rel.value, - selected: relValue === rel.value - }); - }); - - return relListItems; - } - - function buildTargetList(targetValue) { - var targetListItems = [{ - text: 'None', - value: '' - }]; - - if (!editor.settings.target_list) { - targetListItems.push({ - text: 'New window', - value: '_blank' - }); - } - - tinymce.each(editor.settings.target_list, function(target) { - targetListItems.push({ - text: target.text || target.title, - value: target.value, - selected: targetValue === target.value - }); - }); - - return targetListItems; - } - - function buildAnchorListControl(url) { - var anchorList = []; - - tinymce.each(editor.dom.select('a:not([href])'), function(anchor) { - var id = anchor.name || anchor.id; - - if (id) { - anchorList.push({ - text: id, - value: '#' + id, - selected: url.indexOf('#' + id) !== -1 - }); - } - }); - - if (anchorList.length) { - anchorList.unshift({ - text: 'None', - value: '' - }); - - return { - name: 'anchor', - type: 'listbox', - label: 'Anchors', - values: anchorList, - onselect: linkListChangeHandler - }; - } - } - - function updateText() { - if (!initialText && data.text.length === 0) { - this.parent().parent().find('#text')[0].value(this.value()); - } - } - - selectedElm = selection.getNode(); - anchorElm = dom.getParent(selectedElm, 'a[href]'); - - data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'}); - data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; - data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; - data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; - - if (selectedElm.nodeName === "IMG") { - data.text = initialText = " "; - } - - if (linkList) { - linkListCtrl = { - type: 'listbox', - label: 'Link list', - values: buildLinkList(), - onselect: linkListChangeHandler - }; - } - - if (editor.settings.target_list !== false) { - targetListCtrl = { - name: 'target', - type: 'listbox', - label: 'Target', - values: buildTargetList(data.target) - }; - } - - if (editor.settings.rel_list) { - relListCtrl = { - name: 'rel', - type: 'listbox', - label: 'Rel', - values: buildRelList(data.rel) - }; - } - - var injector = angular.element(document.getElementById("umbracoMainPageBody")).injector(); - var dialogService = injector.get("dialogService"); - var currentTarget = null; - - //if we already have a link selected, we want to pass that data over to the dialog - if(anchorElm){ - var anchor = $(anchorElm); - currentTarget = { - name: anchor.attr("title"), - url: anchor.attr("href"), - target: anchor.attr("target") - }; - - //locallink detection, we do this here, to avoid poluting the dialogservice - //so the dialog service can just expect to get a node-like structure - if (currentTarget.url.indexOf("localLink:") > 0) { - var linkId = currentTarget.url.substring(currentTarget.url.indexOf(":") + 1, currentTarget.url.length - 1); - //we need to check if this is an INT or a UDI - var parsedIntId = parseInt(linkId, 10); - if (isNaN(parsedIntId)) { - //it's a UDI - currentTarget.udi = linkId; - } - else { - currentTarget.id = linkId; - } - } - } - - if(onClick) { - onClick(currentTarget, anchorElm); - } - - } - - editor.addButton('link', { - icon: 'link', - tooltip: 'Insert/edit link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]' - }); - - editor.addButton('unlink', { - icon: 'unlink', - tooltip: 'Remove link', - cmd: 'unlink', - stateSelector: 'a[href]' - }); - - editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); - this.showDialog = showDialog; - - editor.addMenuItem('link', { - icon: 'link', - text: 'Insert link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]', - context: 'insert', - prependToContext: true - }); - - }, - - insertLinkInEditor: function(editor, target, anchorElm) { - - var href = target.url; - // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null - var hasUdi = target.udi ? true : false; - var id = hasUdi ? target.udi : (target.id ? target.id : null); - - //Create a json obj used to create the attributes for the tag - function createElemAttributes() { - var a = { - href: href, - title: target.name, - target: target.target ? target.target : null, - rel: target.rel ? target.rel : null - }; - if (hasUdi) { - a["data-udi"] = target.udi; - } - else if (target.id) { - a["data-id"] = target.id; - } - return a; - } - - function insertLink() { - if (anchorElm) { - editor.dom.setAttribs(anchorElm, createElemAttributes()); - - editor.selection.select(anchorElm); - editor.execCommand('mceEndTyping'); - } - else { - editor.execCommand('mceInsertLink', false, createElemAttributes()); - } - } - - if (!href) { - editor.execCommand('unlink'); - return; - } - - //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set - if(id && (angular.isUndefined(target.isMedia) || !target.isMedia)){ - - href = "/{localLink:" + id + "}"; - - insertLink(); - return; - } - - // Is email and not //user@domain.com - if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) { - href = 'mailto:' + href; - insertLink(); - return; - } - - // Is www. prefixed - if (/^\s*www\./i.test(href)) { - href = 'http://' + href; - insertLink(); - return; - } - - insertLink(); - - } - - }; -} - -angular.module('umbraco.services').factory('tinyMceService', tinyMceService); - - -/** - * @ngdoc service - * @name umbraco.services.treeService - * @function - * - * @description - * The tree service factory, used internally by the umbTree and umbTreeItem directives - */ -function treeService($q, treeResource, iconHelper, notificationsService, eventsService) { - - //SD: Have looked at putting this in sessionStorage (not localStorage since that means you wouldn't be able to work - // in multiple tabs) - however our tree structure is cyclical, meaning a node has a reference to it's parent and it's children - // which you cannot serialize to sessionStorage. There's really no benefit of session storage except that you could refresh - // a tab and have the trees where they used to be - supposed that is kind of nice but would mean we'd have to store the parent - // as a nodeid reference instead of a variable with a getParent() method. - var treeCache = {}; - - var standardCssClass = 'icon umb-tree-icon sprTree'; - - function getCacheKey(args) { - //if there is no cache key they return null - it won't be cached. - if (!args || !args.cacheKey) { - return null; - } - - var cacheKey = args.cacheKey; - cacheKey += "_" + args.section; - return cacheKey; - } - - return { - - /** Internal method to return the tree cache */ - _getTreeCache: function() { - return treeCache; - }, - - /** Internal method that ensures there's a routePath, parent and level property on each tree node and adds some icon specific properties so that the nodes display properly */ - _formatNodeDataForUseInUI: function (parentNode, treeNodes, section, level) { - //if no level is set, then we make it 1 - var childLevel = (level ? level : 1); - //set the section if it's not already set - if (!parentNode.section) { - parentNode.section = section; - } - //create a method outside of the loop to return the parent - otherwise jshint blows up - var funcParent = function() { - return parentNode; - }; - for (var i = 0; i < treeNodes.length; i++) { - - treeNodes[i].level = childLevel; - - //create a function to get the parent node, we could assign the parent node but - // then we cannot serialize this entity because we have a cyclical reference. - // Instead we just make a function to return the parentNode. - treeNodes[i].parent = funcParent; - - //set the section for each tree node - this allows us to reference this easily when accessing tree nodes - treeNodes[i].section = section; - - //if there is not route path specified, then set it automatically, - //if this is a tree root node then we want to route to the section's dashboard - if (!treeNodes[i].routePath) { - - if (treeNodes[i].metaData && treeNodes[i].metaData["treeAlias"]) { - //this is a root node - treeNodes[i].routePath = section; - } - else { - var treeAlias = this.getTreeAlias(treeNodes[i]); - treeNodes[i].routePath = section + "/" + treeAlias + "/edit/" + treeNodes[i].id; - } - } - - //now, format the icon data - if (treeNodes[i].iconIsClass === undefined || treeNodes[i].iconIsClass) { - var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNodes[i]); - treeNodes[i].cssClass = standardCssClass + " " + converted; - if (converted.startsWith('.')) { - //its legacy so add some width/height - treeNodes[i].style = "height:16px;width:16px;"; - } - else { - treeNodes[i].style = ""; - } - } - else { - treeNodes[i].style = "background-image: url('" + treeNodes[i].iconFilePath + "');"; - //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this - treeNodes[i].cssClass = standardCssClass + " legacy-custom-file"; - } - } - }, - - /** - * @ngdoc method - * @name umbraco.services.treeService#getTreePackageFolder - * @methodOf umbraco.services.treeService - * @function + * @ngdoc function + * @name umbraco.services.mediaHelper#scaleToMaxSize + * @methodOf umbraco.services.mediaHelper + * @function * * @description - * Determines if the current tree is a plugin tree and if so returns the package folder it has declared - * so we know where to find it's views, otherwise it will just return undefined. + * Finds the corrct max width and max height, given maximum dimensions and keeping aspect ratios * - * @param {String} treeAlias The tree alias to check - */ - getTreePackageFolder: function(treeAlias) { - //we determine this based on the server variables - if (Umbraco.Sys.ServerVariables.umbracoPlugins && - Umbraco.Sys.ServerVariables.umbracoPlugins.trees && - angular.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { - - var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function(item) { - return item.alias === treeAlias; - }); - - return found ? found.packageFolder : undefined; - } - return undefined; - }, - - /** - * @ngdoc method - * @name umbraco.services.treeService#clearCache - * @methodOf umbraco.services.treeService - * @function + * @param {number} maxSize Maximum width & height + * @param {number} width Current width + * @param {number} height Current height + */ + scaleToMaxSize: function (maxSize, width, height) { + var retval = { + width: width, + height: height + }; + var maxWidth = maxSize; + // Max width for the image + var maxHeight = maxSize; + // Max height for the image + var ratio = 0; + // Used for aspect ratio + // Check if the current width is larger than the max + if (width > maxWidth) { + ratio = maxWidth / width; + // get ratio for scaling image + retval.width = maxWidth; + retval.height = height * ratio; + height = height * ratio; + // Reset height to match scaled image + width = width * ratio; // Reset width to match scaled image + } + // Check if current height is larger than max + if (height > maxHeight) { + ratio = maxHeight / height; + // get ratio for scaling image + retval.height = maxHeight; + retval.width = width * ratio; + width = width * ratio; // Reset width to match scaled image + } + return retval; + }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getThumbnailFromPath + * @methodOf umbraco.services.mediaHelper + * @function * * @description - * Clears the tree cache - with optional cacheKey, optional section or optional filter. + * Returns the path to the thumbnail version of a given media library image path * - * @param {Object} args arguments - * @param {String} args.cacheKey optional cachekey - this is used to clear specific trees in dialogs - * @param {String} args.section optional section alias - clear tree for a given section - * @param {String} args.childrenOf optional parent ID - only clear the cache below a specific node - */ - clearCache: function (args) { - //clear all if not specified - if (!args) { - treeCache = {}; - } - else { - //if section and cache key specified just clear that cache - if (args.section && args.cacheKey) { - var cacheKey = getCacheKey(args); - if (cacheKey && treeCache && treeCache[cacheKey] != null) { - treeCache = _.omit(treeCache, cacheKey); - } - } - else if (args.childrenOf) { - //if childrenOf is supplied a cacheKey must be supplied as well - if (!args.cacheKey) { - throw "args.cacheKey is required if args.childrenOf is supplied"; - } - //this will clear out all children for the parentId passed in to this parameter, we'll - // do this by recursing and specifying a filter - var self = this; - this.clearCache({ - cacheKey: args.cacheKey, - filter: function(cc) { - //get the new parent node from the tree cache - var parent = self.getDescendantNode(cc.root, args.childrenOf); - if (parent) { - //clear it's children and set to not expanded - parent.children = null; - parent.expanded = false; - } - //return the cache to be saved - return cc; - } - }); - } - else if (args.filter && angular.isFunction(args.filter)) { - //if a filter is supplied a cacheKey must be supplied as well - if (!args.cacheKey) { - throw "args.cacheKey is required if args.filter is supplied"; - } - - //if a filter is supplied the function needs to return the data to keep - var byKey = treeCache[args.cacheKey]; - if (byKey) { - var result = args.filter(byKey); - - if (result) { - //set the result to the filtered data - treeCache[args.cacheKey] = result; - } - else { - //remove the cache - treeCache = _.omit(treeCache, args.cacheKey); - } - - } - - } - else if (args.cacheKey) { - //if only the cache key is specified, then clear all cache starting with that key - var allKeys1 = _.keys(treeCache); - var toRemove1 = _.filter(allKeys1, function (k) { - return k.startsWith(args.cacheKey + "_"); - }); - treeCache = _.omit(treeCache, toRemove1); - } - else if (args.section) { - //if only the section is specified then clear all cache regardless of cache key by that section - var allKeys2 = _.keys(treeCache); - var toRemove2 = _.filter(allKeys2, function (k) { - return k.endsWith("_" + args.section); - }); - treeCache = _.omit(treeCache, toRemove2); - } - } - }, - - /** - * @ngdoc method - * @name umbraco.services.treeService#loadNodeChildren - * @methodOf umbraco.services.treeService - * @function + * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg + */ + getThumbnailFromPath: function (imagePath) { + //If the path is not an image we cannot get a thumb + if (!this.detectIfImageByExtension(imagePath)) { + return null; + } + //get the proxy url for big thumbnails (this ensures one is always generated) + var thumbnailUrl = umbRequestHelper.getApiUrl('imagesApiBaseUrl', 'GetBigThumbnail', [{ originalImagePath: imagePath }]); + //var ext = imagePath.substr(imagePath.lastIndexOf('.')); + //return imagePath.substr(0, imagePath.lastIndexOf('.')) + "_big-thumb" + ".jpg"; + return thumbnailUrl; + }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#detectIfImageByExtension + * @methodOf umbraco.services.mediaHelper + * @function * * @description - * Clears all node children, gets it's up-to-date children from the server and re-assigns them and then - * returns them in a promise. - * @param {object} args An arguments object - * @param {object} args.node The tree node - * @param {object} args.section The current section - */ - loadNodeChildren: function(args) { - if (!args) { - throw "No args object defined for loadNodeChildren"; - } - if (!args.node) { - throw "No node defined on args object for loadNodeChildren"; - } - - this.removeChildNodes(args.node); - args.node.loading = true; - - return this.getChildren(args) - .then(function(data) { - - //set state to done and expand (only if there actually are children!) - args.node.loading = false; - args.node.children = data; - if (args.node.children && args.node.children.length > 0) { - args.node.expanded = true; - args.node.hasChildren = true; - } - return data; - - }, function(reason) { - - //in case of error, emit event - eventsService.emit("treeService.treeNodeLoadError", {error: reason } ); - - //stop show the loading indicator - args.node.loading = false; - - //tell notications about the error - notificationsService.error(reason); - - return reason; - }); - - }, - - /** - * @ngdoc method - * @name umbraco.services.treeService#removeNode - * @methodOf umbraco.services.treeService + * Returns true/false, indicating if the given path has an allowed image extension + * + * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg + */ + detectIfImageByExtension: function (imagePath) { + if (!imagePath) { + return false; + } + var lowered = imagePath.toLowerCase(); + var ext = lowered.substr(lowered.lastIndexOf('.') + 1); + return (',' + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ',').indexOf(',' + ext + ',') !== -1; + }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#formatFileTypes + * @methodOf umbraco.services.mediaHelper * @function * * @description - * Removes a given node from the tree - * @param {object} treeNode the node to remove - */ - removeNode: function(treeNode) { - if (!angular.isFunction(treeNode.parent)) { - return; - } - - if (treeNode.parent() == null) { - throw "Cannot remove a node that doesn't have a parent"; - } - //remove the current item from it's siblings - treeNode.parent().children.splice(treeNode.parent().children.indexOf(treeNode), 1); - }, - - /** - * @ngdoc method - * @name umbraco.services.treeService#removeChildNodes - * @methodOf umbraco.services.treeService - * @function + * Returns a string with correctly formated file types for ng-file-upload * - * @description - * Removes all child nodes from a given tree node - * @param {object} treeNode the node to remove children from - */ - removeChildNodes : function(treeNode) { - treeNode.expanded = false; - treeNode.children = []; - treeNode.hasChildren = false; - }, - - /** - * @ngdoc method - * @name umbraco.services.treeService#getChildNode - * @methodOf umbraco.services.treeService + * @param {string} file types, ex: jpg,png,tiff + */ + formatFileTypes: function (fileTypes) { + var fileTypesArray = fileTypes.split(','); + var newFileTypesArray = []; + for (var i = 0; i < fileTypesArray.length; i++) { + var fileType = fileTypesArray[i]; + if (fileType.indexOf('.') !== 0) { + fileType = '.'.concat(fileType); + } + newFileTypesArray.push(fileType); + } + return newFileTypesArray.join(','); + }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getFileExtension + * @methodOf umbraco.services.mediaHelper * @function * * @description - * Gets a child node with a given ID, from a specific treeNode - * @param {object} treeNode to retrive child node from - * @param {int} id id of child node - */ - getChildNode: function (treeNode, id) { - if (!treeNode.children) { - return null; - } - var found = _.find(treeNode.children, function (child) { - return String(child.id).toLowerCase() === String(id).toLowerCase(); - }); - return found === undefined ? null : found; - }, - - /** - * @ngdoc method - * @name umbraco.services.treeService#getDescendantNode - * @methodOf umbraco.services.treeService - * @function + * Returns file extension * - * @description - * Gets a descendant node by id - * @param {object} treeNode to retrive descendant node from - * @param {int} id id of descendant node - * @param {string} treeAlias - optional tree alias, if fetching descendant node from a child of a listview document - */ - getDescendantNode: function(treeNode, id, treeAlias) { - - //validate if it is a section container since we'll need a treeAlias if it is one - if (treeNode.isContainer === true && !treeAlias) { - throw "Cannot get a descendant node from a section container node without a treeAlias specified"; - } - - //if it is a section container, we need to find the tree to be searched - if (treeNode.isContainer) { - var foundRoot = null; - for (var c = 0; c < treeNode.children.length; c++) { - if (this.getTreeAlias(treeNode.children[c]) === treeAlias) { - foundRoot = treeNode.children[c]; - break; - } - } - if (!foundRoot) { - throw "Could not find a tree in the current section with alias " + treeAlias; - } - treeNode = foundRoot; - } - - //check this node - if (treeNode.id === id) { - return treeNode; - } - - //check the first level - var found = this.getChildNode(treeNode, id); - if (found) { - return found; - } - - //check each child of this node - if (!treeNode.children) { - return null; - } - - for (var i = 0; i < treeNode.children.length; i++) { - if (treeNode.children[i].children && angular.isArray(treeNode.children[i].children) && treeNode.children[i].children.length > 0) { - //recurse - found = this.getDescendantNode(treeNode.children[i], id); - if (found) { - return found; - } - } - } - - //not found - return found === undefined ? null : found; - }, - - /** + * @param {string} filePath File path, ex /media/1234/my-image.jpg + */ + getFileExtension: function (filePath) { + if (!filePath) { + return false; + } + var lowered = filePath.toLowerCase(); + var ext = lowered.substr(lowered.lastIndexOf('.') + 1); + return ext; + } + }; + } + angular.module('umbraco.services').factory('mediaHelper', mediaHelper); + /** + * @ngdoc service + * @name umbraco.services.mediaTypeHelper + * @description A helper service for the media types + **/ + function mediaTypeHelper(mediaTypeResource, $q) { + var mediaTypeHelperService = { + isFolderType: function (mediaEntity) { + if (!mediaEntity) { + throw 'mediaEntity is null'; + } + if (!mediaEntity.contentTypeAlias) { + throw 'mediaEntity.contentTypeAlias is null'; + } + //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" + //this is the exact same logic that is performed in MediaController.GetChildFolders + return mediaEntity.contentTypeAlias.endsWith('Folder'); + }, + getAllowedImagetypes: function (mediaId) { + //TODO: This is horribly inneficient - why make one request per type!? + //This should make a call to c# to get exactly what it's looking for instead of returning every single media type and doing + //some filtering on the client side. + //This is also called multiple times when it's not needed! Example, when launching the media picker, this will be called twice + //which means we'll be making at least 6 REST calls to fetch each media type + // Get All allowedTypes + return mediaTypeResource.getAllowedTypes(mediaId).then(function (types) { + var allowedQ = types.map(function (type) { + return mediaTypeResource.getById(type.id); + }); + // Get full list + return $q.all(allowedQ).then(function (fullTypes) { + // Find all the media types with an Image Cropper property editor + var filteredTypes = mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper']); + // If there is only one media type with an Image Cropper we will return this one + if (filteredTypes.length === 1) { + return filteredTypes; // If there is more than one Image cropper, custom media types have been added, and we return all media types with and Image cropper or UploadField + } else { + return mediaTypeHelperService.getTypeWithEditor(fullTypes, [ + 'Umbraco.ImageCropper', + 'Umbraco.UploadField' + ]); + } + }); + }); + }, + getTypeWithEditor: function (types, editors) { + return types.filter(function (mediatype) { + for (var i = 0; i < mediatype.groups.length; i++) { + var group = mediatype.groups[i]; + for (var j = 0; j < group.properties.length; j++) { + var property = group.properties[j]; + if (editors.indexOf(property.editor) !== -1) { + return mediatype; + } + } + } + }); + } + }; + return mediaTypeHelperService; + } + angular.module('umbraco.services').factory('mediaTypeHelper', mediaTypeHelper); + /** + * @ngdoc service + * @name umbraco.services.umbracoMenuActions + * + * @requires q + * @requires treeService + * + * @description + * Defines the methods that are called when menu items declare only an action to execute + */ + function umbracoMenuActions($q, treeService, $location, navigationService, appState) { + return { + /** * @ngdoc method - * @name umbraco.services.treeService#getTreeRoot - * @methodOf umbraco.services.treeService + * @name umbraco.services.umbracoMenuActions#RefreshNode + * @methodOf umbraco.services.umbracoMenuActions * @function * * @description - * Gets the root node of the current tree type for a given tree node - * @param {object} treeNode to retrive tree root node from - */ - getTreeRoot: function (treeNode) { - if (!treeNode) { - throw "treeNode cannot be null"; - } - - //all root nodes have metadata key 'treeAlias' - var root = null; - var current = treeNode; - while (root === null && current) { - - if (current.metaData && current.metaData["treeAlias"]) { - root = current; - } - else if (angular.isFunction(current.parent)) { - //we can only continue if there is a parent() method which means this - // tree node was loaded in as part of a real tree, not just as a single tree - // node from the server. - current = current.parent(); - } - else { - current = null; - } - } - return root; - }, - - /** Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node */ - /** + * Clears all node children and then gets it's up-to-date children from the server and re-assigns them + * @param {object} args An arguments object + * @param {object} args.entity The basic entity being acted upon + * @param {object} args.treeAlias The tree alias associated with this entity + * @param {object} args.section The current section + */ + 'RefreshNode': function (args) { + ////just in case clear any tree cache for this node/section + //treeService.clearCache({ + // cacheKey: "__" + args.section, //each item in the tree cache is cached by the section name + // childrenOf: args.entity.parentId //clear the children of the parent + //}); + //since we're dealing with an entity, we need to attempt to find it's tree node, in the main tree + // this action is purely a UI thing so if for whatever reason there is no loaded tree node in the UI + // we can safely ignore this process. + //to find a visible tree node, we'll go get the currently loaded root node from appState + var treeRoot = appState.getTreeState('currentRootNode'); + if (treeRoot && treeRoot.root) { + var treeNode = treeService.getDescendantNode(treeRoot.root, args.entity.id, args.treeAlias); + if (treeNode) { + treeService.loadNodeChildren({ + node: treeNode, + section: args.section + }); + } + } + }, + /** * @ngdoc method - * @name umbraco.services.treeService#getTreeAlias - * @methodOf umbraco.services.treeService + * @name umbraco.services.umbracoMenuActions#CreateChildEntity + * @methodOf umbraco.services.umbracoMenuActions * @function * * @description - * Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node - * @param {object} treeNode to retrive tree alias from - */ - getTreeAlias : function(treeNode) { - var root = this.getTreeRoot(treeNode); - if (root) { - return root.metaData["treeAlias"]; - } - return ""; - }, - - /** + * This will re-route to a route for creating a new entity as a child of the current node + * @param {object} args An arguments object + * @param {object} args.entity The basic entity being acted upon + * @param {object} args.treeAlias The tree alias associated with this entity + * @param {object} args.section The current section + */ + 'CreateChildEntity': function (args) { + navigationService.hideNavigation(); + var route = '/' + args.section + '/' + args.treeAlias + '/edit/' + args.entity.id; + //change to new path + $location.path(route).search({ create: true }); + } + }; + } + angular.module('umbraco.services').factory('umbracoMenuActions', umbracoMenuActions); + (function () { + 'use strict'; + function miniEditorHelper(dialogService, editorState, fileManager, contentEditingHelper, $q) { + var launched = false; + function launchMiniEditor(node) { + var deferred = $q.defer(); + launched = true; + //We need to store the current files selected in the file manager locally because the fileManager + // is a singleton and is shared globally. The mini dialog will also be referencing the fileManager + // and we don't want it to be sharing the same files as the main editor. So we'll store the current files locally here, + // clear them out and then launch the dialog. When the dialog closes, we'll reset the fileManager to it's previous state. + var currFiles = _.groupBy(fileManager.getFiles(), 'alias'); + fileManager.clearFiles(); + //We need to store the original editorState entity because it will need to change when the mini editor is loaded so that + // any property editors that are working with editorState get given the correct entity, otherwise strange things will + // start happening. + var currEditorState = editorState.getCurrent(); + dialogService.open({ + template: 'views/common/dialogs/content/edit.html', + id: node.id, + closeOnSave: true, + tabFilter: ['Generic properties'], + callback: function (data) { + //set the node name back + node.name = data.name; + //reset the fileManager to what it was + fileManager.clearFiles(); + _.each(currFiles, function (val, key) { + fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { + return i.file; + })); + }); + //reset the editor state + editorState.set(currEditorState); + //Now we need to check if the content item that was edited was actually the same content item + // as the main content editor and if so, update all property data + if (data.id === currEditorState.id) { + var changed = contentEditingHelper.reBindChangedProperties(currEditorState, data); + } + launched = false; + deferred.resolve(data); + }, + closeCallback: function () { + //reset the fileManager to what it was + fileManager.clearFiles(); + _.each(currFiles, function (val, key) { + fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { + return i.file; + })); + }); + //reset the editor state + editorState.set(currEditorState); + launched = false; + deferred.reject(); + } + }); + return deferred.promise; + } + var service = { launchMiniEditor: launchMiniEditor }; + return service; + } + angular.module('umbraco.services').factory('miniEditorHelper', miniEditorHelper); + }()); + /** + * @ngdoc service + * @name umbraco.services.navigationService + * + * @requires $rootScope + * @requires $routeParams + * @requires $log + * @requires $location + * @requires dialogService + * @requires treeService + * @requires sectionResource + * + * @description + * Service to handle the main application navigation. Responsible for invoking the tree + * Section navigation and search, and maintain their state for the entire application lifetime + * + */ + function navigationService($rootScope, $routeParams, $log, $location, $q, $timeout, $injector, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) { + //used to track the current dialog object + var currentDialog = null; + //the main tree event handler, which gets assigned via the setupTreeEvents method + var mainTreeEventHandler = null; + //tracks the user profile dialog + var userDialog = null; + function setMode(mode) { + switch (mode) { + case 'tree': + appState.setGlobalState('navMode', 'tree'); + appState.setGlobalState('showNavigation', true); + appState.setMenuState('showMenu', false); + appState.setMenuState('showMenuDialog', false); + appState.setGlobalState('stickyNavigation', false); + appState.setGlobalState('showTray', false); + //$("#search-form input").focus(); + break; + case 'menu': + appState.setGlobalState('navMode', 'menu'); + appState.setGlobalState('showNavigation', true); + appState.setMenuState('showMenu', true); + appState.setMenuState('showMenuDialog', false); + appState.setGlobalState('stickyNavigation', true); + break; + case 'dialog': + appState.setGlobalState('navMode', 'dialog'); + appState.setGlobalState('stickyNavigation', true); + appState.setGlobalState('showNavigation', true); + appState.setMenuState('showMenu', false); + appState.setMenuState('showMenuDialog', true); + break; + case 'search': + appState.setGlobalState('navMode', 'search'); + appState.setGlobalState('stickyNavigation', false); + appState.setGlobalState('showNavigation', true); + appState.setMenuState('showMenu', false); + appState.setSectionState('showSearchResults', true); + appState.setMenuState('showMenuDialog', false); + //TODO: This would be much better off in the search field controller listening to appState changes + $timeout(function () { + $('#search-field').focus(); + }); + break; + default: + appState.setGlobalState('navMode', 'default'); + appState.setMenuState('showMenu', false); + appState.setMenuState('showMenuDialog', false); + appState.setSectionState('showSearchResults', false); + appState.setGlobalState('stickyNavigation', false); + appState.setGlobalState('showTray', false); + if (appState.getGlobalState('isTablet') === true) { + appState.setGlobalState('showNavigation', false); + } + break; + } + } + var service = { + /** initializes the navigation service */ + init: function () { + //keep track of the current section - initially this will always be undefined so + // no point in setting it now until it changes. + $rootScope.$watch(function () { + return $routeParams.section; + }, function (newVal, oldVal) { + appState.setSectionState('currentSection', newVal); + }); + }, + /** * @ngdoc method - * @name umbraco.services.treeService#getTree - * @methodOf umbraco.services.treeService - * @function + * @name umbraco.services.navigationService#load + * @methodOf umbraco.services.navigationService * * @description - * gets the tree, returns a promise - * @param {object} args Arguments - * @param {string} args.section Section alias - * @param {string} args.cacheKey Optional cachekey - */ - getTree: function (args) { - - var deferred = $q.defer(); - - //set defaults - if (!args) { - args = { section: 'content', cacheKey: null }; - } - else if (!args.section) { - args.section = 'content'; - } - - var cacheKey = getCacheKey(args); - - //return the cache if it exists - if (cacheKey && treeCache[cacheKey] !== undefined) { - deferred.resolve(treeCache[cacheKey]); - return deferred.promise; - } - - var self = this; - treeResource.loadApplication(args) - .then(function(data) { - //this will be called once the tree app data has loaded - var result = { - name: data.name, - alias: args.section, - root: data - }; - //we need to format/modify some of the node data to be used in our app. - self._formatNodeDataForUseInUI(result.root, result.root.children, args.section); - - //cache this result if a cache key is specified - generally a cache key should ONLY - // be specified for application trees, dialog trees should not be cached. - if (cacheKey) { - treeCache[cacheKey] = result; - deferred.resolve(treeCache[cacheKey]); - } - - //return un-cached - deferred.resolve(result); - }); - - return deferred.promise; - }, - - /** + * Shows the legacy iframe and loads in the content based on the source url + * @param {String} source The URL to load into the iframe + */ + loadLegacyIFrame: function (source) { + $location.path('/' + appState.getSectionState('currentSection') + '/framed/' + encodeURIComponent(source)); + }, + /** * @ngdoc method - * @name umbraco.services.treeService#getMenu - * @methodOf umbraco.services.treeService - * @function + * @name umbraco.services.navigationService#changeSection + * @methodOf umbraco.services.navigationService * * @description - * Returns available menu actions for a given tree node - * @param {object} args Arguments - * @param {string} args.treeNode tree node object to retrieve the menu for - */ - getMenu: function (args) { - - if (!args) { - throw "args cannot be null"; - } - if (!args.treeNode) { - throw "args.treeNode cannot be null"; - } - - return treeResource.loadMenu(args.treeNode) - .then(function(data) { - //need to convert the icons to new ones - for (var i = 0; i < data.length; i++) { - data[i].cssclass = iconHelper.convertFromLegacyIcon(data[i].cssclass); - } - return data; - }); - }, - - /** + * Changes the active section to a given section alias + * If the navigation is 'sticky' this will load the associated tree + * and load the dashboard related to the section + * @param {string} sectionAlias The alias of the section + */ + changeSection: function (sectionAlias, force) { + setMode('default-opensection'); + if (force && appState.getSectionState('currentSection') === sectionAlias) { + appState.setSectionState('currentSection', ''); + } + appState.setSectionState('currentSection', sectionAlias); + this.showTree(sectionAlias); + $location.path(sectionAlias); + }, + /** * @ngdoc method - * @name umbraco.services.treeService#getChildren - * @methodOf umbraco.services.treeService - * @function + * @name umbraco.services.navigationService#showTree + * @methodOf umbraco.services.navigationService * * @description - * Gets the children from the server for a given node - * @param {object} args Arguments - * @param {object} args.node tree node object to retrieve the children for - * @param {string} args.section current section alias - */ - getChildren: function (args) { - - if (!args) { - throw "No args object defined for getChildren"; - } - if (!args.node) { - throw "No node defined on args object for getChildren"; - } - - var section = args.section || 'content'; - var treeItem = args.node; - - var self = this; - - return treeResource.loadNodes({ node: treeItem }) - .then(function (data) { - //now that we have the data, we need to add the level property to each item and the view - self._formatNodeDataForUseInUI(treeItem, data, section, treeItem.level + 1); - return data; - }); - }, - - /** + * Displays the tree for a given section alias but turning on the containing dom element + * only changes if the section is different from the current one + * @param {string} sectionAlias The alias of the section to load + * @param {Object} syncArgs Optional object of arguments for syncing the tree for the section being shown + */ + showTree: function (sectionAlias, syncArgs) { + if (sectionAlias !== appState.getSectionState('currentSection')) { + appState.setSectionState('currentSection', sectionAlias); + if (syncArgs) { + this.syncTree(syncArgs); + } + } + setMode('tree'); + }, + showTray: function () { + appState.setGlobalState('showTray', true); + }, + hideTray: function () { + appState.setGlobalState('showTray', false); + }, + /** + Called to assign the main tree event handler - this is called by the navigation controller. + TODO: Potentially another dev could call this which would kind of mung the whole app so potentially there's a better way. + */ + setupTreeEvents: function (treeEventHandler) { + mainTreeEventHandler = treeEventHandler; + //when a tree is loaded into a section, we need to put it into appState + mainTreeEventHandler.bind('treeLoaded', function (ev, args) { + appState.setTreeState('currentRootNode', args.tree); + }); + //when a tree node is synced this event will fire, this allows us to set the currentNode + mainTreeEventHandler.bind('treeSynced', function (ev, args) { + if (args.activate === undefined || args.activate === true) { + //set the current selected node + appState.setTreeState('selectedNode', args.node); + //when a node is activated, this is the same as clicking it and we need to set the + //current menu item to be this node as well. + appState.setMenuState('currentNode', args.node); + } + }); + //this reacts to the options item in the tree + mainTreeEventHandler.bind('treeOptionsClick', function (ev, args) { + ev.stopPropagation(); + ev.preventDefault(); + //Set the current action node (this is not the same as the current selected node!) + appState.setMenuState('currentNode', args.node); + if (args.event && args.event.altKey) { + args.skipDefault = true; + } + service.showMenu(ev, args); + }); + mainTreeEventHandler.bind('treeNodeAltSelect', function (ev, args) { + ev.stopPropagation(); + ev.preventDefault(); + args.skipDefault = true; + service.showMenu(ev, args); + }); + //this reacts to tree items themselves being clicked + //the tree directive should not contain any handling, simply just bubble events + mainTreeEventHandler.bind('treeNodeSelect', function (ev, args) { + var n = args.node; + ev.stopPropagation(); + ev.preventDefault(); + if (n.metaData && n.metaData['jsClickCallback'] && angular.isString(n.metaData['jsClickCallback']) && n.metaData['jsClickCallback'] !== '') { + //this is a legacy tree node! + var jsPrefix = 'javascript:'; + var js; + if (n.metaData['jsClickCallback'].startsWith(jsPrefix)) { + js = n.metaData['jsClickCallback'].substr(jsPrefix.length); + } else { + js = n.metaData['jsClickCallback']; + } + try { + var func = eval(js); + //this is normally not necessary since the eval above should execute the method and will return nothing. + if (func != null && typeof func === 'function') { + func.call(); + } + } catch (ex) { + $log.error('Error evaluating js callback from legacy tree node: ' + ex); + } + } else if (n.routePath) { + //add action to the history service + historyService.add({ + name: n.name, + link: n.routePath, + icon: n.icon + }); + //put this node into the tree state + appState.setTreeState('selectedNode', args.node); + //when a node is clicked we also need to set the active menu node to this node + appState.setMenuState('currentNode', args.node); + //not legacy, lets just set the route value and clear the query string if there is one. + $location.path(n.routePath).search(''); + } else if (args.element.section) { + $location.path(args.element.section).search(''); + } + service.hideNavigation(); + }); + }, + /** * @ngdoc method - * @name umbraco.services.treeService#reloadNode - * @methodOf umbraco.services.treeService - * @function + * @name umbraco.services.navigationService#syncTree + * @methodOf umbraco.services.navigationService * * @description - * Re-loads the single node from the server - * @param {object} node Tree node to reload - */ - reloadNode: function(node) { - if (!node) { - throw "node cannot be null"; - } - if (!node.parent()) { - throw "cannot reload a single node without a parent"; - } - if (!node.section) { - throw "cannot reload a single node without an assigned node.section"; - } - - var deferred = $q.defer(); - - //set the node to loading - node.loading = true; - - this.getChildren({ node: node.parent(), section: node.section }).then(function(data) { - - //ok, now that we have the children, find the node we're reloading - var found = _.find(data, function(item) { - return item.id === node.id; - }); - if (found) { - //now we need to find the node in the parent.children collection to replace - var index = _.indexOf(node.parent().children, node); - //the trick here is to not actually replace the node - this would cause the delete animations - //to fire, instead we're just going to replace all the properties of this node. - - //there should always be a method assigned but we'll check anyways - if (angular.isFunction(node.parent().children[index].updateNodeData)) { - node.parent().children[index].updateNodeData(found); - } - else { - //just update as per normal - this means styles, etc.. won't be applied - _.extend(node.parent().children[index], found); - } - - //set the node loading - node.parent().children[index].loading = false; - //return - deferred.resolve(node.parent().children[index]); - } - else { - deferred.reject(); - } - }, function() { - deferred.reject(); - }); - - return deferred.promise; - }, - - /** + * Syncs a tree with a given path, returns a promise + * The path format is: ["itemId","itemId"], and so on + * so to sync to a specific document type node do: + *
    +         * navigationService.syncTree({tree: 'content', path: ["-1","123d"], forceReload: true});
    +         * 
    + * @param {Object} args arguments passed to the function + * @param {String} args.tree the tree alias to sync to + * @param {Array} args.path the path to sync the tree to + * @param {Boolean} args.forceReload optional, specifies whether to force reload the node data from the server even if it already exists in the tree currently + * @param {Boolean} args.activate optional, specifies whether to set the synced node to be the active node, this will default to true if not specified + */ + syncTree: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.path) { + throw 'args.path cannot be null'; + } + if (!args.tree) { + throw 'args.tree cannot be null'; + } + if (mainTreeEventHandler) { + //returns a promise + return mainTreeEventHandler.syncTree(args); + } + //couldn't sync + return angularHelper.rejectedPromise(); + }, + /** + Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to + have to set an active tree and then sync, the new API does this in one method by using syncTree + */ + _syncPath: function (path, forceReload) { + if (mainTreeEventHandler) { + mainTreeEventHandler.syncTree({ + path: path, + forceReload: forceReload + }); + } + }, + //TODO: This should return a promise + reloadNode: function (node) { + if (mainTreeEventHandler) { + mainTreeEventHandler.reloadNode(node); + } + }, + //TODO: This should return a promise + reloadSection: function (sectionAlias) { + if (mainTreeEventHandler) { + mainTreeEventHandler.clearCache({ section: sectionAlias }); + mainTreeEventHandler.load(sectionAlias); + } + }, + /** + Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to + have to set an active tree and then sync, the new API does this in one method by using syncTreePath + */ + _setActiveTreeType: function (treeAlias, loadChildren) { + if (mainTreeEventHandler) { + mainTreeEventHandler._setActiveTreeType(treeAlias, loadChildren); + } + }, + /** * @ngdoc method - * @name umbraco.services.treeService#getPath - * @methodOf umbraco.services.treeService - * @function + * @name umbraco.services.navigationService#hideTree + * @methodOf umbraco.services.navigationService * * @description - * This will return the current node's path by walking up the tree - * @param {object} node Tree node to retrieve path for - */ - getPath: function(node) { - if (!node) { - throw "node cannot be null"; - } - if (!angular.isFunction(node.parent)) { - throw "node.parent is not a function, the path cannot be resolved"; - } - //all root nodes have metadata key 'treeAlias' - var reversePath = []; - var current = node; - while (current != null) { - reversePath.push(current.id); - if (current.metaData && current.metaData["treeAlias"]) { - current = null; - } - else { - current = current.parent(); - } - } - return reversePath.reverse(); - }, - - syncTree: function(args) { - - if (!args) { - throw "No args object defined for syncTree"; - } - if (!args.node) { - throw "No node defined on args object for syncTree"; - } - if (!args.path) { - throw "No path defined on args object for syncTree"; - } - if (!angular.isArray(args.path)) { - throw "Path must be an array"; - } - if (args.path.length < 1) { - //if there is no path, make -1 the path, and that should sync the tree root - args.path.push("-1"); - } - - var deferred = $q.defer(); - - //get the rootNode for the current node, we'll sync based on that - var root = this.getTreeRoot(args.node); - if (!root) { - throw "Could not get the root tree node based on the node passed in"; - } - - //now we want to loop through the ids in the path, first we'll check if the first part - //of the path is the root node, otherwise we'll search it's children. - var currPathIndex = 0; - //if the first id is the root node and there's only one... then consider it synced - if (String(args.path[currPathIndex]).toLowerCase() === String(args.node.id).toLowerCase()) { - if (args.path.length === 1) { - //return the root - deferred.resolve(root); - return deferred.promise; - } - else { - //move to the next path part and continue - currPathIndex = 1; - } - } - - //now that we have the first id to lookup, we can start the process - - var self = this; - var node = args.node; - - var doSync = function () { - //check if it exists in the already loaded children - var child = self.getChildNode(node, args.path[currPathIndex]); - if (child) { - if (args.path.length === (currPathIndex + 1)) { - //woot! synced the node - if (!args.forceReload) { - deferred.resolve(child); - } - else { - //even though we've found the node if forceReload is specified - //we want to go update this single node from the server - self.reloadNode(child).then(function (reloaded) { - deferred.resolve(reloaded); - }, function () { - deferred.reject(); - }); - } - } - else { - //now we need to recurse with the updated node/currPathIndex - currPathIndex++; - node = child; - //recurse - doSync(); - } - } - else { - //couldn't find it in the - self.loadNodeChildren({ node: node, section: node.section }).then(function () { - //ok, got the children, let's find it - var found = self.getChildNode(node, args.path[currPathIndex]); - if (found) { - if (args.path.length === (currPathIndex + 1)) { - //woot! synced the node - deferred.resolve(found); - } - else { - //now we need to recurse with the updated node/currPathIndex - currPathIndex++; - node = found; - //recurse - doSync(); - } - } - else { - //fail! - deferred.reject(); - } - }, function () { - //fail! - deferred.reject(); - }); - } - }; - - //start - doSync(); - - return deferred.promise; - - } - - }; -} - -angular.module('umbraco.services').factory('treeService', treeService); -/** -* @ngdoc service -* @name umbraco.services.umbRequestHelper -* @description A helper object used for sending requests to the server -**/ -function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService, eventsService) { - return { - - /** + * Hides the tree by hiding the containing dom element + */ + hideTree: function () { + if (appState.getGlobalState('isTablet') === true && !appState.getGlobalState('stickyNavigation')) { + //reset it to whatever is in the url + appState.setSectionState('currentSection', $routeParams.section); + setMode('default-hidesectiontree'); + } + }, + /** * @ngdoc method - * @name umbraco.services.umbRequestHelper#convertVirtualToAbsolutePath - * @methodOf umbraco.services.umbRequestHelper - * @function + * @name umbraco.services.navigationService#showMenu + * @methodOf umbraco.services.navigationService * * @description - * This will convert a virtual path (i.e. ~/App_Plugins/Blah/Test.html ) to an absolute path - * - * @param {string} a virtual path, if this is already an absolute path it will just be returned, if this is a relative path an exception will be thrown - */ - convertVirtualToAbsolutePath: function(virtualPath) { - if (virtualPath.startsWith("/")) { - return virtualPath; - } - if (!virtualPath.startsWith("~/")) { - throw "The path " + virtualPath + " is not a virtual path"; - } - if (!Umbraco.Sys.ServerVariables.application.applicationPath) { - throw "No applicationPath defined in Umbraco.ServerVariables.application.applicationPath"; - } - return Umbraco.Sys.ServerVariables.application.applicationPath + virtualPath.trimStart("~/"); - }, - - /** + * Hides the tree by hiding the containing dom element. + * This always returns a promise! + * + * @param {Event} event the click event triggering the method, passed from the DOM element + */ + showMenu: function (event, args) { + var deferred = $q.defer(); + var self = this; + treeService.getMenu({ treeNode: args.node }).then(function (data) { + //check for a default + //NOTE: event will be undefined when a call to hideDialog is made so it won't re-load the default again. + // but perhaps there's a better way to deal with with an additional parameter in the args ? it works though. + if (data.defaultAlias && !args.skipDefault) { + var found = _.find(data.menuItems, function (item) { + return item.alias = data.defaultAlias; + }); + if (found) { + //NOTE: This is assigning the current action node - this is not the same as the currently selected node! + appState.setMenuState('currentNode', args.node); + //ensure the current dialog is cleared before creating another! + if (currentDialog) { + dialogService.close(currentDialog); + } + var dialog = self.showDialog({ + node: args.node, + action: found, + section: appState.getSectionState('currentSection') + }); + //return the dialog this is opening. + deferred.resolve(dialog); + return; + } + } + //there is no default or we couldn't find one so just continue showing the menu + setMode('menu'); + appState.setMenuState('currentNode', args.node); + appState.setMenuState('menuActions', data.menuItems); + appState.setMenuState('dialogTitle', args.node.name); + //we're not opening a dialog, return null. + deferred.resolve(null); + }); + return deferred.promise; + }, + /** * @ngdoc method - * @name umbraco.services.umbRequestHelper#dictionaryToQueryString - * @methodOf umbraco.services.umbRequestHelper - * @function + * @name umbraco.services.navigationService#hideMenu + * @methodOf umbraco.services.navigationService * * @description - * This will turn an array of key/value pairs into a query string - * - * @param {Array} queryStrings An array of key/value pairs - */ - dictionaryToQueryString: function (queryStrings) { - - if (angular.isArray(queryStrings)) { - return _.map(queryStrings, function (item) { - var key = null; - var val = null; - for (var k in item) { - key = k; - val = item[k]; - break; - } - if (key === null || val === null) { - throw "The object in the array was not formatted as a key/value pair"; - } - return encodeURIComponent(key) + "=" + encodeURIComponent(val); - }).join("&"); - } - else if (angular.isObject(queryStrings)) { - - //this allows for a normal object to be passed in (ie. a dictionary) - return decodeURIComponent($.param(queryStrings)); - } - - throw "The queryString parameter is not an array or object of key value pairs"; - }, - - /** + * Hides the menu by hiding the containing dom element + */ + hideMenu: function () { + //SD: Would we ever want to access the last action'd node instead of clearing it here? + appState.setMenuState('currentNode', null); + appState.setMenuState('menuActions', []); + setMode('tree'); + }, + /** Executes a given menu action */ + executeMenuAction: function (action, node, section) { + if (!action) { + throw 'action cannot be null'; + } + if (!node) { + throw 'node cannot be null'; + } + if (!section) { + throw 'section cannot be null'; + } + if (action.metaData && action.metaData['actionRoute'] && angular.isString(action.metaData['actionRoute'])) { + //first check if the menu item simply navigates to a route + var parts = action.metaData['actionRoute'].split('?'); + $location.path(parts[0]).search(parts.length > 1 ? parts[1] : ''); + this.hideNavigation(); + return; + } else if (action.metaData && action.metaData['jsAction'] && angular.isString(action.metaData['jsAction'])) { + //we'll try to get the jsAction from the injector + var menuAction = action.metaData['jsAction'].split('.'); + if (menuAction.length !== 2) { + //if it is not two parts long then this most likely means that it's a legacy action + var js = action.metaData['jsAction'].replace('javascript:', ''); + //there's not really a different way to acheive this except for eval + eval(js); + } else { + var menuActionService = $injector.get(menuAction[0]); + if (!menuActionService) { + throw 'The angular service ' + menuAction[0] + ' could not be found'; + } + var method = menuActionService[menuAction[1]]; + if (!method) { + throw 'The method ' + menuAction[1] + ' on the angular service ' + menuAction[0] + ' could not be found'; + } + method.apply(this, [{ + //map our content object to a basic entity to pass in to the menu handlers, + //this is required for consistency since a menu item needs to be decoupled from a tree node since the menu can + //exist standalone in the editor for which it can only pass in an entity (not tree node). + entity: umbModelMapper.convertToEntityBasic(node), + action: action, + section: section, + treeAlias: treeService.getTreeAlias(node) + }]); + } + } else { + service.showDialog({ + node: node, + action: action, + section: section + }); + } + }, + /** * @ngdoc method - * @name umbraco.services.umbRequestHelper#getApiUrl - * @methodOf umbraco.services.umbRequestHelper - * @function + * @name umbraco.services.navigationService#showUserDialog + * @methodOf umbraco.services.navigationService * * @description - * This will return the webapi Url for the requested key based on the servervariables collection - * - * @param {string} apiName The webapi name that is found in the servervariables["umbracoUrls"] dictionary - * @param {string} actionName The webapi action name - * @param {object} queryStrings Can be either a string or an array containing key/value pairs - */ - getApiUrl: function (apiName, actionName, queryStrings) { - if (!Umbraco || !Umbraco.Sys || !Umbraco.Sys.ServerVariables || !Umbraco.Sys.ServerVariables["umbracoUrls"]) { - throw "No server variables defined!"; - } - - if (!Umbraco.Sys.ServerVariables["umbracoUrls"][apiName]) { - throw "No url found for api name " + apiName; - } - - return Umbraco.Sys.ServerVariables["umbracoUrls"][apiName] + actionName + - (!queryStrings ? "" : "?" + (angular.isString(queryStrings) ? queryStrings : this.dictionaryToQueryString(queryStrings))); - - }, - - /** - * @ngdoc function - * @name umbraco.services.umbRequestHelper#resourcePromise - * @methodOf umbraco.services.umbRequestHelper - * @function + * Opens the user dialog, next to the sections navigation + * template is located in views/common/dialogs/user.html + */ + showUserDialog: function () { + // hide tray and close help dialog + if (service.helpDialog) { + service.helpDialog.close(); + } + service.hideTray(); + if (service.userDialog) { + service.userDialog.close(); + service.userDialog = undefined; + } + service.userDialog = dialogService.open({ + template: 'views/common/dialogs/user.html', + modalClass: 'umb-modal-left', + show: true + }); + return service.userDialog; + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#showUserDialog + * @methodOf umbraco.services.navigationService * * @description - * This returns a promise with an underlying http call, it is a helper method to reduce - * the amount of duplicate code needed to query http resources and automatically handle any - * 500 Http server errors. + * Opens the user dialog, next to the sections navigation + * template is located in views/common/dialogs/user.html + */ + showHelpDialog: function () { + // hide tray and close user dialog + service.hideTray(); + if (service.userDialog) { + service.userDialog.close(); + } + if (service.helpDialog) { + service.helpDialog.close(); + service.helpDialog = undefined; + } + service.helpDialog = dialogService.open({ + template: 'views/common/dialogs/help.html', + modalClass: 'umb-modal-left', + show: true + }); + return service.helpDialog; + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#showDialog + * @methodOf umbraco.services.navigationService * - * @param {object} opts A mixed object which can either be a `string` representing the error message to be - * returned OR an `object` containing either: - * { success: successCallback, errorMsg: errorMessage } - * OR - * { success: successCallback, error: errorCallback } - * In both of the above, the successCallback must accept these parameters: `data`, `status`, `headers`, `config` - * If using the errorCallback it must accept these parameters: `data`, `status`, `headers`, `config` - * The success callback must return the data which will be resolved by the deferred object. - * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } - */ - resourcePromise: function (httpPromise, opts) { - var deferred = $q.defer(); - - /** The default success callback used if one is not supplied in the opts */ - function defaultSuccess(data, status, headers, config) { - //when it's successful, just return the data - return data; - } - - /** The default error callback used if one is not supplied in the opts */ - function defaultError(data, status, headers, config) { - return { - //NOTE: the default error message here should never be used based on the above docs! - errorMsg: (angular.isString(opts) ? opts : 'An error occurred!'), - data: data, - status: status - }; - } - - //create the callbacs based on whats been passed in. - var callbacks = { - success: ((!opts || !opts.success) ? defaultSuccess : opts.success), - error: ((!opts || !opts.error) ? defaultError : opts.error) - }; - - httpPromise.success(function (data, status, headers, config) { - - //invoke the callback - var result = callbacks.success.apply(this, [data, status, headers, config]); - - //when it's successful, just return the data - deferred.resolve(result); - - }).error(function (data, status, headers, config) { - - //invoke the callback - var result = callbacks.error.apply(this, [data, status, headers, config]); - - //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. - if (status >= 500 && status < 600) { - - //show a ysod dialog - if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { - eventsService.emit('app.ysod', - { - errorMsg: 'An error occured', - data: data - }); - } - else { - //show a simple error notification - notificationsService.error("Server error", "Contact administrator, see log for full details.
    " + result.errorMsg + ""); - } - - } - - //return an error object including the error message for UI - deferred.reject({ - errorMsg: result.errorMsg, - data: result.data, - status: result.status - }); - - - }); - - return deferred.promise; - - }, - - /** Used for saving media/content specifically */ - postSaveContent: function (args) { - - if (!args.restApiUrl) { - throw "args.restApiUrl is a required argument"; - } - if (!args.content) { - throw "args.content is a required argument"; - } - if (!args.action) { - throw "args.action is a required argument"; - } - if (!args.files) { - throw "args.files is a required argument"; - } - if (!args.dataFormatter) { - throw "args.dataFormatter is a required argument"; - } - - - var deferred = $q.defer(); - - //save the active tab id so we can set it when the data is returned. - var activeTab = _.find(args.content.tabs, function (item) { - return item.active; - }); - var activeTabIndex = (activeTab === undefined ? 0 : _.indexOf(args.content.tabs, activeTab)); - - //save the data - this.postMultiPartRequest( - args.restApiUrl, - { key: "contentItem", value: args.dataFormatter(args.content, args.action) }, - function (data, formData) { - //now add all of the assigned files - for (var f in args.files) { - //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key - // so we know which property it belongs to on the server side - formData.append("file_" + args.files[f].alias, args.files[f].file); - } - - }, - function (data, status, headers, config) { - //success callback - - //reset the tabs and set the active one - _.each(data.tabs, function (item) { - item.active = false; - }); - data.tabs[activeTabIndex].active = true; - - //the data returned is the up-to-date data so the UI will refresh - deferred.resolve(data); - }, - function (data, status, headers, config) { - //failure callback - - //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. - if (status >= 500 && status < 600) { - - //This is a bit of a hack to check if the error is due to a file being uploaded that is too large, - // we have to just check for the existence of a string value but currently that is the best way to - // do this since it's very hacky/difficult to catch this on the server - if (typeof data !== "undefined" && typeof data.indexOf === "function" && data.indexOf("Maximum request length exceeded") >= 0) { - notificationsService.error("Server error", "The uploaded file was too large, check with your site administrator to adjust the maximum size allowed"); - } - else if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { - //show a ysod dialog - eventsService.emit('app.ysod', - { - errorMsg: 'An error occured', - data: data - }); - } - else { - //show a simple error notification - notificationsService.error("Server error", "Contact administrator, see log for full details.
    " + data.ExceptionMessage + ""); - } - - } - - //return an error object including the error message for UI - deferred.reject({ - errorMsg: 'An error occurred', - data: data, - status: status - }); - - - }); - - return deferred.promise; - }, - - /** Posts a multi-part mime request to the server */ - postMultiPartRequest: function (url, jsonData, transformCallback, successCallback, failureCallback) { - - //validate input, jsonData can be an array of key/value pairs or just one key/value pair. - if (!jsonData) { throw "jsonData cannot be null"; } - - if (angular.isArray(jsonData)) { - _.each(jsonData, function (item) { - if (!item.key || !item.value) { throw "jsonData array item must have both a key and a value property"; } - }); - } - else if (!jsonData.key || !jsonData.value) { throw "jsonData object must have both a key and a value property"; } - - - $http({ - method: 'POST', - url: url, - //IMPORTANT!!! You might think this should be set to 'multipart/form-data' but this is not true because when we are sending up files - // the request needs to include a 'boundary' parameter which identifies the boundary name between parts in this multi-part request - // and setting the Content-type manually will not set this boundary parameter. For whatever reason, setting the Content-type to 'false' - // will force the request to automatically populate the headers properly including the boundary parameter. - headers: { 'Content-Type': false }, - transformRequest: function (data) { - var formData = new FormData(); - //add the json data - if (angular.isArray(data)) { - _.each(data, function (item) { - formData.append(item.key, !angular.isString(item.value) ? angular.toJson(item.value) : item.value); - }); - } - else { - formData.append(data.key, !angular.isString(data.value) ? angular.toJson(data.value) : data.value); - } - - //call the callback - if (transformCallback) { - transformCallback.apply(this, [data, formData]); - } - - return formData; - }, - data: jsonData - }). - success(function (data, status, headers, config) { - if (successCallback) { - successCallback.apply(this, [data, status, headers, config]); - } - }). - error(function (data, status, headers, config) { - if (failureCallback) { - failureCallback.apply(this, [data, status, headers, config]); - } - }); - } - }; -} -angular.module('umbraco.services').factory('umbRequestHelper', umbRequestHelper); - -angular.module('umbraco.services') - .factory('userService', function ($rootScope, eventsService, $q, $location, $log, securityRetryQueue, authResource, dialogService, $timeout, angularHelper, $http) { - - var currentUser = null; - var lastUserId = null; - var loginDialog = null; - - //this tracks the last date/time that the user's remainingAuthSeconds was updated from the server - // this is used so that we know when to go and get the user's remaining seconds directly. - var lastServerTimeoutSet = null; - - function openLoginDialog(isTimedOut) { - if (!loginDialog) { - loginDialog = dialogService.open({ - - //very special flag which means that global events cannot close this dialog - manualClose: true, - - template: 'views/common/dialogs/login.html', - modalClass: "login-overlay", - animation: "slide", - show: true, - callback: onLoginDialogClose, - dialogData: { - isTimedOut: isTimedOut - } - }); - } - } - - function onLoginDialogClose(success) { - loginDialog = null; - - if (success) { - securityRetryQueue.retryAll(currentUser.name); - } - else { - securityRetryQueue.cancelAll(); - $location.path('/'); - } - } - - /** - This methods will set the current user when it is resolved and - will then start the counter to count in-memory how many seconds they have - remaining on the auth session - */ - function setCurrentUser(usr) { - if (!usr.remainingAuthSeconds) { - throw "The user object is invalid, the remainingAuthSeconds is required."; - } - currentUser = usr; - lastServerTimeoutSet = new Date(); - //start the timer - countdownUserTimeout(); - } - - /** - Method to count down the current user's timeout seconds, - this will continually count down their current remaining seconds every 5 seconds until - there are no more seconds remaining. - */ - function countdownUserTimeout() { - - $timeout(function () { - - if (currentUser) { - //countdown by 5 seconds since that is how long our timer is for. - currentUser.remainingAuthSeconds -= 5; - - //if there are more than 30 remaining seconds, recurse! - if (currentUser.remainingAuthSeconds > 30) { - - //we need to check when the last time the timeout was set from the server, if - // it has been more than 30 seconds then we'll manually go and retrieve it from the - // server - this helps to keep our local countdown in check with the true timeout. - if (lastServerTimeoutSet != null) { - var now = new Date(); - var seconds = (now.getTime() - lastServerTimeoutSet.getTime()) / 1000; - - if (seconds > 30) { - - //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we - // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait. - lastServerTimeoutSet = null; - - //now go get it from the server - //NOTE: the safeApply because our timeout is set to not run digests (performance reasons) - angularHelper.safeApply($rootScope, function () { - authResource.getRemainingTimeoutSeconds().then(function (result) { - setUserTimeoutInternal(result); - }); - }); - } - } - - //recurse the countdown! - countdownUserTimeout(); - } - else { - - //we are either timed out or very close to timing out so we need to show the login dialog. - if (Umbraco.Sys.ServerVariables.umbracoSettings.keepUserLoggedIn !== true) { - //NOTE: the safeApply because our timeout is set to not run digests (performance reasons) - angularHelper.safeApply($rootScope, function () { - try { - //NOTE: We are calling this again so that the server can create a log that the timeout has expired, we - // don't actually care about this result. - authResource.getRemainingTimeoutSeconds(); - } - finally { - userAuthExpired(); - } - }); - } - else { - //we've got less than 30 seconds remaining so let's check the server - - if (lastServerTimeoutSet != null) { - //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we - // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait. - lastServerTimeoutSet = null; - - //now go get it from the server - //NOTE: the safeApply because our timeout is set to not run digests (performance reasons) - angularHelper.safeApply($rootScope, function () { - authResource.getRemainingTimeoutSeconds().then(function (result) { - setUserTimeoutInternal(result); - }); - }); - } - - //recurse the countdown! - countdownUserTimeout(); - - } - } - } - }, 5000, //every 5 seconds - false); //false = do NOT execute a digest for every iteration - } - - /** Called to update the current user's timeout */ - function setUserTimeoutInternal(newTimeout) { - - - var asNumber = parseFloat(newTimeout); - if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) { - currentUser.remainingAuthSeconds = newTimeout; - lastServerTimeoutSet = new Date(); - } - } - - /** resets all user data, broadcasts the notAuthenticated event and shows the login dialog */ - function userAuthExpired(isLogout) { - //store the last user id and clear the user - if (currentUser && currentUser.id !== undefined) { - lastUserId = currentUser.id; - } - - if (currentUser) { - currentUser.remainingAuthSeconds = 0; - } - - lastServerTimeoutSet = null; - currentUser = null; - - //broadcast a global event that the user is no longer logged in - eventsService.emit("app.notAuthenticated"); - - openLoginDialog(isLogout === undefined ? true : !isLogout); - } - - // Register a handler for when an item is added to the retry queue - securityRetryQueue.onItemAddedCallbacks.push(function (retryItem) { - if (securityRetryQueue.hasMore()) { - userAuthExpired(); - } - }); - - return { - - /** Internal method to display the login dialog */ - _showLoginDialog: function () { - openLoginDialog(); - }, - /** Returns a promise, sends a request to the server to check if the current cookie is authorized */ - isAuthenticated: function () { - //if we've got a current user then just return true - if (currentUser) { - var deferred = $q.defer(); - deferred.resolve(true); - return deferred.promise; - } - return authResource.isAuthenticated(); - }, - - /** Returns a promise, sends a request to the server to validate the credentials */ - authenticate: function (login, password) { - - return authResource.performLogin(login, password) - .then(this.setAuthenticationSuccessful); - }, - setAuthenticationSuccessful:function (data) { - - //when it's successful, return the user data - setCurrentUser(data); - - var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "credentials" }; - - //broadcast a global event - eventsService.emit("app.authenticated", result); - return result; - }, - - /** Logs the user out - */ - logout: function () { - - return authResource.performLogout() - .then(function(data) { - userAuthExpired(); - //done! - return null; - }); - }, - - /** Returns the current user object in a promise */ - getCurrentUser: function (args) { - var deferred = $q.defer(); - - if (!currentUser) { - authResource.getCurrentUser() - .then(function (data) { - - var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "implicit" }; - - //TODO: This is a mega backwards compatibility hack... These variables SHOULD NOT exist in the server variables - // since they are not supposed to be dynamic but I accidentally added them there in 7.1.5 IIRC so some people might - // now be relying on this :( - if (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables) { - Umbraco.Sys.ServerVariables["security"] = { - startContentId: data.startContentId, - startMediaId: data.startMediaId - }; - } - - if (args && args.broadcastEvent) { - //broadcast a global event, will inform listening controllers to load in the user specific data - eventsService.emit("app.authenticated", result); - } - - setCurrentUser(data); - - deferred.resolve(currentUser); - }); - - } - else { - deferred.resolve(currentUser); - } - - return deferred.promise; - }, - - /** Called whenever a server request is made that contains a x-umb-user-seconds response header for which we can update the user's remaining timeout seconds */ - setUserTimeout: function (newTimeout) { - setUserTimeoutInternal(newTimeout); - } - }; - - }); - -/*Contains multiple services for various helper tasks */ -function versionHelper() { - - return { - - //see: https://gist.github.com/TheDistantSea/8021359 - versionCompare: function(v1, v2, options) { - var lexicographical = options && options.lexicographical, - zeroExtend = options && options.zeroExtend, - v1parts = v1.split('.'), - v2parts = v2.split('.'); - - function isValidPart(x) { - return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x); - } - - if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { - return NaN; - } - - if (zeroExtend) { - while (v1parts.length < v2parts.length) { - v1parts.push("0"); - } - while (v2parts.length < v1parts.length) { - v2parts.push("0"); - } - } - - if (!lexicographical) { - v1parts = v1parts.map(Number); - v2parts = v2parts.map(Number); - } - - for (var i = 0; i < v1parts.length; ++i) { - if (v2parts.length === i) { - return 1; - } - - if (v1parts[i] === v2parts[i]) { - continue; - } - else if (v1parts[i] > v2parts[i]) { - return 1; - } - else { - return -1; - } - } - - if (v1parts.length !== v2parts.length) { - return -1; - } - - return 0; - } - }; -} -angular.module('umbraco.services').factory('versionHelper', versionHelper); - -function dateHelper() { - - return { - - convertToServerStringTime: function(momentLocal, serverOffsetMinutes, format) { - - //get the formatted offset time in HH:mm (server time offset is in minutes) - var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + - moment() - .startOf('day') - .minutes(Math.abs(serverOffsetMinutes)) - .format('HH:mm'); - - var server = moment.utc(momentLocal).utcOffset(formattedOffset); - return server.format(format ? format : "YYYY-MM-DD HH:mm:ss"); - }, - - convertToLocalMomentTime: function (strVal, serverOffsetMinutes) { - - //get the formatted offset time in HH:mm (server time offset is in minutes) - var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + - moment() - .startOf('day') - .minutes(Math.abs(serverOffsetMinutes)) - .format('HH:mm'); - - //convert to the iso string format - var isoFormat = moment(strVal).format("YYYY-MM-DDTHH:mm:ss") + formattedOffset; - - //create a moment with the iso format which will include the offset with the correct time - // then convert it to local time - return moment.parseZone(isoFormat).local(); - } - - }; -} -angular.module('umbraco.services').factory('dateHelper', dateHelper); - -function packageHelper(assetsService, treeService, eventsService, $templateCache) { - - return { - - /** Called when a package is installed, this resets a bunch of data and ensures the new package assets are loaded in */ - packageInstalled: function () { - - //clears the tree - treeService.clearCache(); - - //clears the template cache - $templateCache.removeAll(); - - //emit event to notify anything else - eventsService.emit("app.reInitialize"); - } - - }; -} -angular.module('umbraco.services').factory('packageHelper', packageHelper); - -//TODO: I believe this is obsolete -function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) { - return { - /** sets the image's url, thumbnail and if its a folder */ - setImageData: function(img) { - - img.isFolder = !mediaHelper.hasFilePropertyType(img); - - if(!img.isFolder){ - img.thumbnail = mediaHelper.resolveFile(img, true); - img.image = mediaHelper.resolveFile(img, false); - } - }, - - /** sets the images original size properties - will check if it is a folder and if so will just make it square */ - setOriginalSize: function(img, maxHeight) { - //set to a square by default - img.originalWidth = maxHeight; - img.originalHeight = maxHeight; - - var widthProp = _.find(img.properties, function(v) { return (v.alias === "umbracoWidth"); }); - if (widthProp && widthProp.value) { - img.originalWidth = parseInt(widthProp.value, 10); - if (isNaN(img.originalWidth)) { - img.originalWidth = maxHeight; - } - } - var heightProp = _.find(img.properties, function(v) { return (v.alias === "umbracoHeight"); }); - if (heightProp && heightProp.value) { - img.originalHeight = parseInt(heightProp.value, 10); - if (isNaN(img.originalHeight)) { - img.originalHeight = maxHeight; - } - } - }, - - /** sets the image style which get's used in the angular markup */ - setImageStyle: function(img, width, height, rightMargin, bottomMargin) { - img.style = { width: width + "px", height: height + "px", "margin-right": rightMargin + "px", "margin-bottom": bottomMargin + "px" }; - img.thumbStyle = { - "background-image": "url('" + img.thumbnail + "')", - "background-repeat": "no-repeat", - "background-position": "center", - "background-size": Math.min(width, img.originalWidth) + "px " + Math.min(height, img.originalHeight) + "px" - }; - }, - - /** gets the image's scaled wdith based on the max row height */ - getScaledWidth: function(img, maxHeight) { - var scaled = img.originalWidth * maxHeight / img.originalHeight; - return scaled; - //round down, we don't want it too big even by half a pixel otherwise it'll drop to the next row - //return Math.floor(scaled); - }, - - /** returns the target row width taking into account how many images will be in the row and removing what the margin is */ - getTargetWidth: function(imgsPerRow, maxRowWidth, margin) { - //take into account the margin, we will have 1 less margin item than we have total images - return (maxRowWidth - ((imgsPerRow - 1) * margin)); - }, - - /** - This will determine the row/image height for the next collection of images which takes into account the - ideal image count per row. It will check if a row can be filled with this ideal count and if not - if there - are additional images available to fill the row it will keep calculating until they fit. - - It will return the calculated height and the number of images for the row. - - targetHeight = optional; - */ - getRowHeightForImages: function(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) { - - var idealImages = imgs.slice(0, idealImgPerRow); - //get the target row width without margin - var targetRowWidth = this.getTargetWidth(idealImages.length, maxRowWidth, margin); - //this gets the image with the smallest height which equals the maximum we can scale up for this image block - var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight); - //if the max scale height is smaller than the min display height, we'll use the min display height - targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); - - var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); - - if (attemptedRowHeight != null) { - - //if this is smaller than the min display then we need to use the min display, - // which means we'll need to remove one from the row so we can scale up to fill the row - if (attemptedRowHeight < minDisplayHeight) { - - if (idealImages.length > 1) { - - //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight - targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); - return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight); - } - else { - //this will occur when we only have one image remaining in the row but it's still going to be too wide even when - // using the minimum display height specified. In this case we're going to have to just crop the image in it's center - // using the minimum display height and the full row width - return { height: minDisplayHeight, imgCount: 1 }; - } - } - else { - //success! - return { height: attemptedRowHeight, imgCount: idealImages.length }; - } - } - - //we know the width will fit in a row, but we now need to figure out if we can fill - // the entire row in the case that we have more images remaining than the idealImgPerRow. - - if (idealImages.length === imgs.length) { - //we have no more remaining images to fill the space, so we'll just use the calc height - return { height: targetHeight, imgCount: idealImages.length }; - } - else if (idealImages.length === 1) { - //this will occur when we only have one image remaining in the row to process but it's not really going to fit ideally - // in the row. - return { height: minDisplayHeight, imgCount: 1 }; - } - else if (idealImages.length === idealImgPerRow && targetHeight < maxRowHeight) { - - //if we're already dealing with the ideal images per row and it's not quite wide enough, we can scale up a little bit so - // long as the targetHeight is currently less than the maxRowHeight. The scale up will be half-way between our current - // target height and the maxRowHeight (we won't loop forever though - if there's a difference of 5 px we'll just quit) - - while (targetHeight < maxRowHeight && (maxRowHeight - targetHeight) > 5) { - targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); - attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); - if (attemptedRowHeight != null) { - //success! - return { height: attemptedRowHeight, imgCount: idealImages.length }; - } - } - - //Ok, we couldn't actually scale it up with the ideal row count we'll just recurse with a lesser image count. - return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin); - } - else if (targetHeight === maxRowHeight) { - - //This is going to happen when: - // * We can fit a list of images in a row, but they come up too short (based on minDisplayHeight) - // * Then we'll try to remove an image, but when we try to scale to fit, the width comes up too narrow but the images are already at their - // maximum height (maxRowHeight) - // * So we're stuck, we cannot precicely fit the current list of images, so we'll render a row that will be max height but won't be wide enough - // which is better than rendering a row that is shorter than the minimum since that could be quite small. - - return { height: targetHeight, imgCount: idealImages.length }; - } - else { - - //we have additional images so we'll recurse and add 1 to the idealImgPerRow until it fits - return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow + 1, margin); - } - - }, - - performGetRowHeight: function(idealImages, targetRowWidth, minDisplayHeight, targetHeight) { - - var currRowWidth = 0; - - for (var i = 0; i < idealImages.length; i++) { - var scaledW = this.getScaledWidth(idealImages[i], targetHeight); - currRowWidth += scaledW; - } - - if (currRowWidth > targetRowWidth) { - //get the new scaled height to fit - var newHeight = targetRowWidth * targetHeight / currRowWidth; - - return newHeight; - } - else if (idealImages.length === 1 && (currRowWidth <= targetRowWidth) && !idealImages[0].isFolder) { - //if there is only one image, then return the target height - return targetHeight; - } - else if (currRowWidth / targetRowWidth > 0.90) { - //it's close enough, it's at least 90% of the width so we'll accept it with the target height - return targetHeight; - } - else { - //if it's not successful, return null - return null; - } - }, - - /** builds an image grid row */ - buildRow: function(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) { - var currRowWidth = 0; - var row = { images: [] }; - - var imageRowHeight = this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); - var targetWidth = this.getTargetWidth(imageRowHeight.imgCount, maxRowWidth, margin); - - var sizes = []; - //loop through the images we know fit into the height - for (var i = 0; i < imageRowHeight.imgCount; i++) { - //get the lower width to ensure it always fits - var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height)); - - if (currRowWidth + scaledWidth <= targetWidth) { - currRowWidth += scaledWidth; - sizes.push({ - width:scaledWidth, - //ensure that the height is rounded - height: Math.round(imageRowHeight.height) - }); - row.images.push(imgs[i]); - } - else if (imageRowHeight.imgCount === 1 && row.images.length === 0) { - //the image is simply too wide, we'll crop/center it - sizes.push({ - width: maxRowWidth, - //ensure that the height is rounded - height: Math.round(imageRowHeight.height) - }); - row.images.push(imgs[i]); - } - else { - //the max width has been reached - break; - } - } - - //loop through the images for the row and apply the styles - for (var j = 0; j < row.images.length; j++) { - var bottomMargin = margin; - //make the margin 0 for the last one - if (j === (row.images.length - 1)) { - margin = 0; - } - this.setImageStyle(row.images[j], sizes[j].width, sizes[j].height, margin, bottomMargin); - } - - if (row.images.length === 1 && totalRemaining > 1) { - //if there's only one image on the row and there are more images remaining, set the container to max width - row.images[0].style.width = maxRowWidth + "px"; - } - - - return row; - }, - - /** Returns the maximum image scaling height for the current image collection */ - getMaxScaleableHeight: function(imgs, maxRowHeight) { - - var smallestHeight = _.min(imgs, function(item) { return item.originalHeight; }).originalHeight; - - //adjust the smallestHeight if it is larger than the static max row height - if (smallestHeight > maxRowHeight) { - smallestHeight = maxRowHeight; - } - return smallestHeight; - }, - - /** Creates the image grid with calculated widths/heights for images to fill the grid nicely */ - buildGrid: function(images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin,imagesOnly) { - - var rows = []; - var imagesProcessed = 0; - - //first fill in all of the original image sizes and URLs - for (var i = startingIndex; i < images.length; i++) { - var item = images[i]; - - this.setImageData(item); - this.setOriginalSize(item, maxRowHeight); - - if(imagesOnly && !item.isFolder && !item.thumbnail){ - images.splice(i, 1); - i--; - } - } - - while ((imagesProcessed + startingIndex) < images.length) { - //get the maxHeight for the current un-processed images - var currImgs = images.slice(imagesProcessed); - - //build the row - var remaining = images.length - imagesProcessed; - var row = this.buildRow(currImgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, remaining); - if (row.images.length > 0) { - rows.push(row); - imagesProcessed += row.images.length; - } - else { - - if (currImgs.length > 0) { - throw "Could not fill grid with all images, images remaining: " + currImgs.length; - } - - //if there was nothing processed, exit - break; - } - } - - return rows; - } - }; -} -angular.module("umbraco.services").factory("umbPhotoFolderHelper", umbPhotoFolderHelper); - -/** - * @ngdoc function - * @name umbraco.services.umbModelMapper - * @function + * @description + * Opens a dialog, for a given action on a given tree node + * uses the dialogService to inject the selected action dialog + * into #dialog div.umb-panel-body + * the path to the dialog view is determined by: + * "views/" + current tree + "/" + action alias + ".html" + * The dialog controller will get passed a scope object that is created here with the properties: + * scope.currentNode = the selected tree node + * scope.currentAction = the selected menu item + * so that the dialog controllers can use these properties + * + * @param {Object} args arguments passed to the function + * @param {Scope} args.scope current scope passed to the dialog + * @param {Object} args.action the clicked action containing `name` and `alias` + */ + showDialog: function (args) { + if (!args) { + throw 'showDialog is missing the args parameter'; + } + if (!args.action) { + throw 'The args parameter must have an \'action\' property as the clicked menu action object'; + } + if (!args.node) { + throw 'The args parameter must have a \'node\' as the active tree node'; + } + //ensure the current dialog is cleared before creating another! + if (currentDialog) { + dialogService.close(currentDialog); + currentDialog = null; + } + setMode('dialog'); + //NOTE: Set up the scope object and assign properties, this is legacy functionality but we have to live with it now. + // we should be passing in currentNode and currentAction using 'dialogData' for the dialog, not attaching it to a scope. + // This scope instance will be destroyed by the dialog so it cannot be a scope that exists outside of the dialog. + // If a scope instance has been passed in, we'll have to create a child scope of it, otherwise a new root scope. + var dialogScope = args.scope ? args.scope.$new() : $rootScope.$new(); + dialogScope.currentNode = args.node; + dialogScope.currentAction = args.action; + //the title might be in the meta data, check there first + if (args.action.metaData['dialogTitle']) { + appState.setMenuState('dialogTitle', args.action.metaData['dialogTitle']); + } else { + appState.setMenuState('dialogTitle', args.action.name); + } + var templateUrl; + var iframe; + if (args.action.metaData['actionUrl']) { + templateUrl = args.action.metaData['actionUrl']; + iframe = true; + } else if (args.action.metaData['actionView']) { + templateUrl = args.action.metaData['actionView']; + iframe = false; + } else { + //by convention we will look into the /views/{treetype}/{action}.html + // for example: /views/content/create.html + //we will also check for a 'packageName' for the current tree, if it exists then the convention will be: + // for example: /App_Plugins/{mypackage}/backoffice/{treetype}/create.html + var treeAlias = treeService.getTreeAlias(args.node); + var packageTreeFolder = treeService.getTreePackageFolder(treeAlias); + if (!treeAlias) { + throw 'Could not get tree alias for node ' + args.node.id; + } + if (packageTreeFolder) { + templateUrl = Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + '/' + packageTreeFolder + '/backoffice/' + treeAlias + '/' + args.action.alias + '.html'; + } else { + templateUrl = 'views/' + treeAlias + '/' + args.action.alias + '.html'; + } + iframe = false; + } + //TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with + // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog + // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, + // though would be v-easy, just not sure we want to ever support that? + var dialog = dialogService.open({ + container: $('#dialog div.umb-modalcolumn-body'), + //The ONLY reason we're passing in scope to the dialogService (which is legacy functionality) is + // for backwards compatibility since many dialogs require $scope.currentNode or $scope.currentAction + // to exist + scope: dialogScope, + inline: true, + show: true, + iframe: iframe, + modalClass: 'umb-dialog', + template: templateUrl, + //These will show up on the dialog controller's $scope under dialogOptions + currentNode: args.node, + currentAction: args.action + }); + //save the currently assigned dialog so it can be removed before a new one is created + currentDialog = dialog; + return dialog; + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#hideDialog + * @methodOf umbraco.services.navigationService + * + * @description + * hides the currently open dialog + */ + hideDialog: function (showMenu) { + setMode('default'); + if (showMenu) { + this.showMenu(undefined, { + skipDefault: true, + node: appState.getMenuState('currentNode') + }); + } + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#showSearch + * @methodOf umbraco.services.navigationService + * + * @description + * shows the search pane + */ + showSearch: function () { + setMode('search'); + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#hideSearch + * @methodOf umbraco.services.navigationService + * + * @description + * hides the search pane + */ + hideSearch: function () { + setMode('default-hidesearch'); + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#hideNavigation + * @methodOf umbraco.services.navigationService + * + * @description + * hides any open navigation panes and resets the tree, actions and the currently selected node + */ + hideNavigation: function () { + appState.setMenuState('menuActions', []); + setMode('default'); + } + }; + return service; + } + angular.module('umbraco.services').factory('navigationService', navigationService); + /** + * @ngdoc service + * @name umbraco.services.notificationsService * + * @requires $rootScope + * @requires $timeout + * @requires angularHelper + * * @description - * Utility class to map/convert models - */ -function umbModelMapper() { - - return { - - - /** + * Application-wide service for handling notifications, the umbraco application + * maintains a single collection of notications, which the UI watches for changes. + * By default when a notication is added, it is automaticly removed 7 seconds after + * This can be changed on add() + * + * ##usage + * To use, simply inject the notificationsService into any controller that needs it, and make + * sure the umbraco.services module is accesible - which it should be by default. + * + *
    + *		notificationsService.success("Document Published", "hooraaaay for you!");
    + *      notificationsService.error("Document Failed", "booooh");
    + * 
    + */ + angular.module('umbraco.services').factory('notificationsService', function ($rootScope, $timeout, angularHelper) { + var nArray = []; + function setViewPath(view) { + if (view.indexOf('/') < 0) { + view = 'views/common/notifications/' + view; + } + if (view.indexOf('.html') < 0) { + view = view + '.html'; + } + return view; + } + var service = { + /** + * @ngdoc method + * @name umbraco.services.notificationsService#add + * @methodOf umbraco.services.notificationsService + * + * @description + * Lower level api for adding notifcations, support more advanced options + * @param {Object} item The notification item + * @param {String} item.headline Short headline + * @param {String} item.message longer text for the notication, trimmed after 200 characters, which can then be exanded + * @param {String} item.type Notification type, can be: "success","warning","error" or "info" + * @param {String} item.url url to open when notification is clicked + * @param {String} item.view path to custom view to load into the notification box + * @param {Array} item.actions Collection of button actions to append (label, func, cssClass) + * @param {Boolean} item.sticky if set to true, the notification will not auto-close + * @returns {Object} args notification object + */ + add: function (item) { + angularHelper.safeApply($rootScope, function () { + if (item.view) { + item.view = setViewPath(item.view); + item.sticky = true; + item.type = 'form'; + item.headline = null; + } + //add a colon after the headline if there is a message as well + if (item.message) { + item.headline += ': '; + if (item.message.length > 200) { + item.sticky = true; + } + } + //we need to ID the item, going by index isn't good enough because people can remove at different indexes + // whenever they want. Plus once we remove one, then the next index will be different. The only way to + // effectively remove an item is by an Id. + item.id = String.CreateGuid(); + nArray.push(item); + if (!item.sticky) { + $timeout(function () { + var found = _.find(nArray, function (i) { + return i.id === item.id; + }); + if (found) { + var index = nArray.indexOf(found); + nArray.splice(index, 1); + } + }, 7000); + } + return item; + }); + }, + hasView: function (view) { + if (!view) { + return _.find(nArray, function (notification) { + return notification.view; + }); + } else { + view = setViewPath(view).toLowerCase(); + return _.find(nArray, function (notification) { + return notification.view.toLowerCase() === view; + }); + } + }, + addView: function (view, args) { + var item = { + args: args, + view: view + }; + service.add(item); + }, + /** + * @ngdoc method + * @name umbraco.services.notificationsService#showNotification + * @methodOf umbraco.services.notificationsService + * + * @description + * Shows a notification based on the object passed in, normally used to render notifications sent back from the server + * + * @returns {Object} args notification object + */ + showNotification: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (args.type === undefined || args.type === null) { + throw 'args.type cannot be null'; + } + if (!args.header) { + throw 'args.header cannot be null'; + } + switch (args.type) { + case 0: + //save + this.success(args.header, args.message); + break; + case 1: + //info + this.success(args.header, args.message); + break; + case 2: + //error + this.error(args.header, args.message); + break; + case 3: + //success + this.success(args.header, args.message); + break; + case 4: + //warning + this.warning(args.header, args.message); + break; + } + }, + /** + * @ngdoc method + * @name umbraco.services.notificationsService#success + * @methodOf umbraco.services.notificationsService + * + * @description + * Adds a green success notication to the notications collection + * This should be used when an operations *completes* without errors + * + * @param {String} headline Headline of the notification + * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded + * @returns {Object} notification object + */ + success: function (headline, message) { + return service.add({ + headline: headline, + message: message, + type: 'success', + time: new Date() + }); + }, + /** + * @ngdoc method + * @name umbraco.services.notificationsService#error + * @methodOf umbraco.services.notificationsService + * + * @description + * Adds a red error notication to the notications collection + * This should be used when an operations *fails* and could not complete + * + * @param {String} headline Headline of the notification + * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded + * @returns {Object} notification object + */ + error: function (headline, message) { + return service.add({ + headline: headline, + message: message, + type: 'error', + time: new Date() + }); + }, + /** + * @ngdoc method + * @name umbraco.services.notificationsService#warning + * @methodOf umbraco.services.notificationsService + * + * @description + * Adds a yellow warning notication to the notications collection + * This should be used when an operations *completes* but something was not as expected + * + * + * @param {String} headline Headline of the notification + * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded + * @returns {Object} notification object + */ + warning: function (headline, message) { + return service.add({ + headline: headline, + message: message, + type: 'warning', + time: new Date() + }); + }, + /** + * @ngdoc method + * @name umbraco.services.notificationsService#warning + * @methodOf umbraco.services.notificationsService + * + * @description + * Adds a yellow warning notication to the notications collection + * This should be used when an operations *completes* but something was not as expected + * + * + * @param {String} headline Headline of the notification + * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded + * @returns {Object} notification object + */ + info: function (headline, message) { + return service.add({ + headline: headline, + message: message, + type: 'info', + time: new Date() + }); + }, + /** + * @ngdoc method + * @name umbraco.services.notificationsService#remove + * @methodOf umbraco.services.notificationsService + * + * @description + * Removes a notification from the notifcations collection at a given index + * + * @param {Int} index index where the notication should be removed from + */ + remove: function (index) { + if (angular.isObject(index)) { + var i = nArray.indexOf(index); + angularHelper.safeApply($rootScope, function () { + nArray.splice(i, 1); + }); + } else { + angularHelper.safeApply($rootScope, function () { + nArray.splice(index, 1); + }); + } + }, + /** + * @ngdoc method + * @name umbraco.services.notificationsService#removeAll + * @methodOf umbraco.services.notificationsService + * + * @description + * Removes all notifications from the notifcations collection + */ + removeAll: function () { + angularHelper.safeApply($rootScope, function () { + nArray = []; + }); + }, + /** + * @ngdoc property + * @name umbraco.services.notificationsService#current + * @propertyOf umbraco.services.notificationsService + * + * @description + * Returns an array of current notifications to display + * + * @returns {string} returns an array + */ + current: nArray, + /** + * @ngdoc method + * @name umbraco.services.notificationsService#getCurrent + * @methodOf umbraco.services.notificationsService + * + * @description + * Method to return all notifications from the notifcations collection + */ + getCurrent: function () { + return nArray; + } + }; + return service; + }); + (function () { + 'use strict'; + function overlayHelper() { + var numberOfOverlays = 0; + function registerOverlay() { + numberOfOverlays++; + return numberOfOverlays; + } + function unregisterOverlay() { + numberOfOverlays--; + return numberOfOverlays; + } + function getNumberOfOverlays() { + return numberOfOverlays; + } + var service = { + numberOfOverlays: numberOfOverlays, + registerOverlay: registerOverlay, + unregisterOverlay: unregisterOverlay, + getNumberOfOverlays: getNumberOfOverlays + }; + return service; + } + angular.module('umbraco.services').factory('overlayHelper', overlayHelper); + }()); + (function () { + 'use strict'; + function platformService() { + function isMac() { + return navigator.platform.toUpperCase().indexOf('MAC') >= 0; + } + //////////// + var service = { isMac: isMac }; + return service; + } + angular.module('umbraco.services').factory('platformService', platformService); + }()); + /** + * @ngdoc service + * @name umbraco.services.searchService + * + * + * @description + * Service for handling the main application search, can currently search content, media and members + * + * ##usage + * To use, simply inject the searchService into any controller that needs it, and make + * sure the umbraco.services module is accesible - which it should be by default. + * + *
    + *      searchService.searchMembers({term: 'bob'}).then(function(results){
    + *          angular.forEach(results, function(result){
    + *                  //returns:
    + *                  {name: "name", id: 1234, menuUrl: "url", editorPath: "url", metaData: {}, subtitle: "/path/etc" }
    + *           })          
    + *           var result = 
    + *       }) 
    + * 
    + */ + angular.module('umbraco.services').factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper, $injector, searchResultFormatter) { + return { + /** + * @ngdoc method + * @name umbraco.services.searchService#searchMembers + * @methodOf umbraco.services.searchService + * + * @description + * Searches the default member search index + * @param {Object} args argument object + * @param {String} args.term seach term + * @returns {Promise} returns promise containing all matching members + */ + searchMembers: function (args) { + if (!args.term) { + throw 'args.term is required'; + } + return entityResource.search(args.term, 'Member', args.searchFrom).then(function (data) { + _.each(data, function (item) { + searchResultFormatter.configureMemberResult(item); + }); + return data; + }); + }, + /** + * @ngdoc method + * @name umbraco.services.searchService#searchContent + * @methodOf umbraco.services.searchService + * + * @description + * Searches the default internal content search index + * @param {Object} args argument object + * @param {String} args.term seach term + * @returns {Promise} returns promise containing all matching content items + */ + searchContent: function (args) { + if (!args.term) { + throw 'args.term is required'; + } + return entityResource.search(args.term, 'Document', args.searchFrom, args.canceler).then(function (data) { + _.each(data, function (item) { + searchResultFormatter.configureContentResult(item); + }); + return data; + }); + }, + /** + * @ngdoc method + * @name umbraco.services.searchService#searchMedia + * @methodOf umbraco.services.searchService + * + * @description + * Searches the default media search index + * @param {Object} args argument object + * @param {String} args.term seach term + * @returns {Promise} returns promise containing all matching media items + */ + searchMedia: function (args) { + if (!args.term) { + throw 'args.term is required'; + } + return entityResource.search(args.term, 'Media', args.searchFrom).then(function (data) { + _.each(data, function (item) { + searchResultFormatter.configureMediaResult(item); + }); + return data; + }); + }, + /** + * @ngdoc method + * @name umbraco.services.searchService#searchAll + * @methodOf umbraco.services.searchService + * + * @description + * Searches all available indexes and returns all results in one collection + * @param {Object} args argument object + * @param {String} args.term seach term + * @returns {Promise} returns promise containing all matching items + */ + searchAll: function (args) { + if (!args.term) { + throw 'args.term is required'; + } + return entityResource.searchAll(args.term, args.canceler).then(function (data) { + _.each(data, function (resultByType) { + //we need to format the search result data to include things like the subtitle, urls, etc... + // this is done with registered angular services as part of the SearchableTreeAttribute, if that + // is not found, than we format with the default formatter + var formatterMethod = searchResultFormatter.configureDefaultResult; + //check if a custom formatter is specified... + if (resultByType.jsSvc) { + var searchFormatterService = $injector.get(resultByType.jsSvc); + if (searchFormatterService) { + if (!resultByType.jsMethod) { + resultByType.jsMethod = 'format'; + } + formatterMethod = searchFormatterService[resultByType.jsMethod]; + if (!formatterMethod) { + throw 'The method ' + resultByType.jsMethod + ' on the angular service ' + resultByType.jsSvc + ' could not be found'; + } + } + } + //now apply the formatter for each result + _.each(resultByType.results, function (item) { + formatterMethod.apply(this, [ + item, + resultByType.treeAlias, + resultByType.appAlias + ]); + }); + }); + return data; + }); + }, + //TODO: This doesn't do anything! + setCurrent: function (sectionAlias) { + var currentSection = sectionAlias; + } + }; + }); + function searchResultFormatter(umbRequestHelper) { + function configureDefaultResult(content, treeAlias, appAlias) { + content.editorPath = appAlias + '/' + treeAlias + '/edit/' + content.id; + angular.extend(content.metaData, { treeAlias: treeAlias }); + } + function configureContentResult(content, treeAlias, appAlias) { + content.menuUrl = umbRequestHelper.getApiUrl('contentTreeBaseUrl', 'GetMenu', [ + { id: content.id }, + { application: appAlias } + ]); + content.editorPath = appAlias + '/' + treeAlias + '/edit/' + content.id; + angular.extend(content.metaData, { treeAlias: treeAlias }); + content.subTitle = content.metaData.Url; + } + function configureMemberResult(member, treeAlias, appAlias) { + member.menuUrl = umbRequestHelper.getApiUrl('memberTreeBaseUrl', 'GetMenu', [ + { id: member.id }, + { application: appAlias } + ]); + member.editorPath = appAlias + '/' + treeAlias + '/edit/' + (member.key ? member.key : member.id); + angular.extend(member.metaData, { treeAlias: treeAlias }); + member.subTitle = member.metaData.Email; + } + function configureMediaResult(media, treeAlias, appAlias) { + media.menuUrl = umbRequestHelper.getApiUrl('mediaTreeBaseUrl', 'GetMenu', [ + { id: media.id }, + { application: appAlias } + ]); + media.editorPath = appAlias + '/' + treeAlias + '/edit/' + media.id; + angular.extend(media.metaData, { treeAlias: treeAlias }); + } + return { + configureContentResult: configureContentResult, + configureMemberResult: configureMemberResult, + configureMediaResult: configureMediaResult, + configureDefaultResult: configureDefaultResult + }; + } + angular.module('umbraco.services').factory('searchResultFormatter', searchResultFormatter); + /** + * @ngdoc service + * @name umbraco.services.sectionService + * + * + * @description + * A service to return the sections (applications) to be listed in the navigation which are contextual to the current user + */ + (function () { + 'use strict'; + function sectionService(userService, $q, sectionResource) { + function getSectionsForUser() { + var deferred = $q.defer(); + userService.getCurrentUser().then(function (u) { + //if they've already loaded, return them + if (u.sections) { + deferred.resolve(u.sections); + } else { + sectionResource.getSections().then(function (sections) { + //set these to the user (cached), then the user changes, these will be wiped + u.sections = sections; + deferred.resolve(u.sections); + }); + } + }); + return deferred.promise; + } + var service = { getSectionsForUser: getSectionsForUser }; + return service; + } + angular.module('umbraco.services').factory('sectionService', sectionService); + }()); + /** + * @ngdoc service + * @name umbraco.services.serverValidationManager + * @function + * + * @description + * Used to handle server side validation and wires up the UI with the messages. There are 2 types of validation messages, one + * is for user defined properties (called Properties) and the other is for field properties which are attached to the native + * model objects (not user defined). The methods below are named according to these rules: Properties vs Fields. + */ + function serverValidationManager($timeout) { + var callbacks = []; + /** calls the callback specified with the errors specified, used internally */ + function executeCallback(self, errorsForCallback, callback) { + callback.apply(self, [ + false, + //pass in a value indicating it is invalid + errorsForCallback, + //pass in the errors for this item + self.items + ]); //pass in all errors in total + } + function getFieldErrors(self, fieldName) { + if (!angular.isString(fieldName)) { + throw 'fieldName must be a string'; + } + //find errors for this field name + return _.filter(self.items, function (item) { + return item.propertyAlias === null && item.fieldName === fieldName; + }); + } + function getPropertyErrors(self, propertyAlias, fieldName) { + if (!angular.isString(propertyAlias)) { + throw 'propertyAlias must be a string'; + } + if (fieldName && !angular.isString(fieldName)) { + throw 'fieldName must be a string'; + } + //find all errors for this property + return _.filter(self.items, function (item) { + return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === '')); + }); + } + return { + /** + * @ngdoc function + * @name umbraco.services.serverValidationManager#subscribe + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * This method needs to be called once all field and property errors are wired up. + * + * In some scenarios where the error collection needs to be persisted over a route change + * (i.e. when a content item (or any item) is created and the route redirects to the editor) + * the controller should call this method once the data is bound to the scope + * so that any persisted validation errors are re-bound to their controls. Once they are re-binded this then clears the validation + * colleciton so that if another route change occurs, the previously persisted validation errors are not re-bound to the new item. + */ + executeAndClearAllSubscriptions: function () { + var self = this; + $timeout(function () { + for (var cb in callbacks) { + if (callbacks[cb].propertyAlias === null) { + //its a field error callback + var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName); + if (fieldErrors.length > 0) { + executeCallback(self, fieldErrors, callbacks[cb].callback); + } + } else { + //its a property error + var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].fieldName); + if (propErrors.length > 0) { + executeCallback(self, propErrors, callbacks[cb].callback); + } + } + } + //now that they are all executed, we're gonna clear all of the errors we have + self.clear(); + }); + }, + /** + * @ngdoc function + * @name umbraco.services.serverValidationManager#subscribe + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Adds a callback method that is executed whenever validation changes for the field name + property specified. + * This is generally used for server side validation in order to match up a server side validation error with + * a particular field, otherwise we can only pinpoint that there is an error for a content property, not the + * property's specific field. This is used with the val-server directive in which the directive specifies the + * field alias to listen for. + * If propertyAlias is null, then this subscription is for a field property (not a user defined property). + */ + subscribe: function (propertyAlias, fieldName, callback) { + if (!callback) { + return; + } + if (propertyAlias === null) { + //don't add it if it already exists + var exists1 = _.find(callbacks, function (item) { + return item.propertyAlias === null && item.fieldName === fieldName; + }); + if (!exists1) { + callbacks.push({ + propertyAlias: null, + fieldName: fieldName, + callback: callback + }); + } + } else if (propertyAlias !== undefined) { + //don't add it if it already exists + var exists2 = _.find(callbacks, function (item) { + return item.propertyAlias === propertyAlias && item.fieldName === fieldName; + }); + if (!exists2) { + callbacks.push({ + propertyAlias: propertyAlias, + fieldName: fieldName, + callback: callback + }); + } + } + }, + unsubscribe: function (propertyAlias, fieldName) { + if (propertyAlias === null) { + //remove all callbacks for the content field + callbacks = _.reject(callbacks, function (item) { + return item.propertyAlias === null && item.fieldName === fieldName; + }); + } else if (propertyAlias !== undefined) { + //remove all callbacks for the content property + callbacks = _.reject(callbacks, function (item) { + return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === '') && (fieldName === undefined || fieldName === '')); + }); + } + }, + /** + * @ngdoc function + * @name getPropertyCallbacks + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets all callbacks that has been registered using the subscribe method for the propertyAlias + fieldName combo. + * This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an + * explicit field name set. + */ + getPropertyCallbacks: function (propertyAlias, fieldName) { + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly against the field and for only the property + return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === '')); + }); + return found; + }, + /** + * @ngdoc function + * @name getFieldCallbacks + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets all callbacks that has been registered using the subscribe method for the field. + */ + getFieldCallbacks: function (fieldName) { + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly against the field + return item.propertyAlias === null && item.fieldName === fieldName; + }); + return found; + }, + /** + * @ngdoc function + * @name addFieldError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Adds an error message for a native content item field (not a user defined property, for Example, 'Name') + */ + addFieldError: function (fieldName, errorMsg) { + if (!fieldName) { + return; + } + //only add the item if it doesn't exist + if (!this.hasFieldError(fieldName)) { + this.items.push({ + propertyAlias: null, + fieldName: fieldName, + errorMsg: errorMsg + }); + } + //find all errors for this item + var errorsForCallback = getFieldErrors(this, fieldName); + //we should now call all of the call backs registered for this error + var cbs = this.getFieldCallbacks(fieldName); + //call each callback for this error + for (var cb in cbs) { + executeCallback(this, errorsForCallback, cbs[cb].callback); + } + }, + /** + * @ngdoc function + * @name addPropertyError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Adds an error message for the content property + */ + addPropertyError: function (propertyAlias, fieldName, errorMsg) { + if (!propertyAlias) { + return; + } + //only add the item if it doesn't exist + if (!this.hasPropertyError(propertyAlias, fieldName)) { + this.items.push({ + propertyAlias: propertyAlias, + fieldName: fieldName, + errorMsg: errorMsg + }); + } + //find all errors for this item + var errorsForCallback = getPropertyErrors(this, propertyAlias, fieldName); + //we should now call all of the call backs registered for this error + var cbs = this.getPropertyCallbacks(propertyAlias, fieldName); + //call each callback for this error + for (var cb in cbs) { + executeCallback(this, errorsForCallback, cbs[cb].callback); + } + }, + /** + * @ngdoc function + * @name removePropertyError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Removes an error message for the content property + */ + removePropertyError: function (propertyAlias, fieldName) { + if (!propertyAlias) { + return; + } + //remove the item + this.items = _.reject(this.items, function (item) { + return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === '')); + }); + }, + /** + * @ngdoc function + * @name reset + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Clears all errors and notifies all callbacks that all server errros are now valid - used when submitting a form + */ + reset: function () { + this.clear(); + for (var cb in callbacks) { + callbacks[cb].callback.apply(this, [ + true, + //pass in a value indicating it is VALID + [], + //pass in empty collection + [] + ]); //pass in empty collection + } + }, + /** + * @ngdoc function + * @name clear + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Clears all errors + */ + clear: function () { + this.items = []; + }, + /** + * @ngdoc function + * @name getPropertyError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets the error message for the content property + */ + getPropertyError: function (propertyAlias, fieldName) { + var err = _.find(this.items, function (item) { + //return true if the property alias matches and if an empty field name is specified or the field name matches + return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === '')); + }); + return err; + }, + /** + * @ngdoc function + * @name getFieldError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets the error message for a content field + */ + getFieldError: function (fieldName) { + var err = _.find(this.items, function (item) { + //return true if the property alias matches and if an empty field name is specified or the field name matches + return item.propertyAlias === null && item.fieldName === fieldName; + }); + return err; + }, + /** + * @ngdoc function + * @name hasPropertyError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Checks if the content property + field name combo has an error + */ + hasPropertyError: function (propertyAlias, fieldName) { + var err = _.find(this.items, function (item) { + //return true if the property alias matches and if an empty field name is specified or the field name matches + return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === '')); + }); + return err ? true : false; + }, + /** + * @ngdoc function + * @name hasFieldError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Checks if a content field has an error + */ + hasFieldError: function (fieldName) { + var err = _.find(this.items, function (item) { + //return true if the property alias matches and if an empty field name is specified or the field name matches + return item.propertyAlias === null && item.fieldName === fieldName; + }); + return err ? true : false; + }, + /** The array of error messages */ + items: [] + }; + } + angular.module('umbraco.services').factory('serverValidationManager', serverValidationManager); + (function () { + 'use strict'; + function templateHelperService(localizationService) { + //crappy hack due to dictionary items not in umbracoNode table + function getInsertDictionarySnippet(nodeName) { + return '@Umbraco.GetDictionaryValue("' + nodeName + '")'; + } + function getInsertPartialSnippet(parentId, nodeName) { + var partialViewName = nodeName.replace('.cshtml', ''); + if (parentId) { + partialViewName = parentId + '/' + partialViewName; + } + return '@Html.Partial("' + partialViewName + '")'; + } + function getQuerySnippet(queryExpression) { + var code = '\n@{\n' + '\tvar selection = ' + queryExpression + ';\n}\n'; + code += '
      \n' + '\t@foreach(var item in selection){\n' + '\t\t
    • \n' + '\t\t\t@item.Name\n' + '\t\t
    • \n' + '\t}\n' + '
    \n\n'; + return code; + } + function getRenderBodySnippet() { + return '@RenderBody()'; + } + function getRenderSectionSnippet(sectionName, mandatory) { + return '@RenderSection("' + sectionName + '", ' + mandatory + ')'; + } + function getAddSectionSnippet(sectionName) { + return '@section ' + sectionName + '\r\n{\r\n\r\n\t{0}\r\n\r\n}\r\n'; + } + function getGeneralShortcuts() { + return { + 'name': localizationService.localize('shortcuts_generalHeader'), + 'shortcuts': [ + { + 'description': localizationService.localize('buttons_undo'), + 'keys': [ + { 'key': 'ctrl' }, + { 'key': 'z' } + ] + }, + { + 'description': localizationService.localize('buttons_redo'), + 'keys': [ + { 'key': 'ctrl' }, + { 'key': 'y' } + ] + }, + { + 'description': localizationService.localize('buttons_save'), + 'keys': [ + { 'key': 'ctrl' }, + { 'key': 's' } + ] + } + ] + }; + } + function getEditorShortcuts() { + return { + 'name': localizationService.localize('shortcuts_editorHeader'), + 'shortcuts': [ + { + 'description': localizationService.localize('shortcuts_commentLine'), + 'keys': [ + { 'key': 'ctrl' }, + { 'key': '/' } + ] + }, + { + 'description': localizationService.localize('shortcuts_removeLine'), + 'keys': [ + { 'key': 'ctrl' }, + { 'key': 'd' } + ] + }, + { + 'description': localizationService.localize('shortcuts_copyLineUp'), + 'keys': { + 'win': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'up' } + ], + 'mac': [ + { 'key': 'cmd' }, + { 'key': 'alt' }, + { 'key': 'up' } + ] + } + }, + { + 'description': localizationService.localize('shortcuts_copyLineDown'), + 'keys': { + 'win': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'down' } + ], + 'mac': [ + { 'key': 'cmd' }, + { 'key': 'alt' }, + { 'key': 'down' } + ] + } + }, + { + 'description': localizationService.localize('shortcuts_moveLineUp'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'up' } + ] + }, + { + 'description': localizationService.localize('shortcuts_moveLineDown'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'down' } + ] + } + ] + }; + } + function getTemplateEditorShortcuts() { + return { + 'name': 'Umbraco', + //No need to localise Umbraco is the same in all languages :) + 'shortcuts': [ + { + 'description': localizationService.format([ + 'template_insert', + 'template_insertPageField' + ], '%0% %1%'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'v' } + ] + }, + { + 'description': localizationService.format([ + 'template_insert', + 'template_insertPartialView' + ], '%0% %1%'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'p' } + ] + }, + { + 'description': localizationService.format([ + 'template_insert', + 'template_insertDictionaryItem' + ], '%0% %1%'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'd' } + ] + }, + { + 'description': localizationService.format([ + 'template_insert', + 'template_insertMacro' + ], '%0% %1%'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'm' } + ] + }, + { + 'description': localizationService.localize('template_queryBuilder'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'q' } + ] + }, + { + 'description': localizationService.format([ + 'template_insert', + 'template_insertSections' + ], '%0% %1%'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 's' } + ] + }, + { + 'description': localizationService.localize('template_mastertemplate'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 't' } + ] + } + ] + }; + } + function getPartialViewEditorShortcuts() { + return { + 'name': 'Umbraco', + //No need to localise Umbraco is the same in all languages :) + 'shortcuts': [ + { + 'description': localizationService.format([ + 'template_insert', + 'template_insertPageField' + ], '%0% %1%'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'v' } + ] + }, + { + 'description': localizationService.format([ + 'template_insert', + 'template_insertDictionaryItem' + ], '%0% %1%'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'd' } + ] + }, + { + 'description': localizationService.format([ + 'template_insert', + 'template_insertMacro' + ], '%0% %1%'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'm' } + ] + }, + { + 'description': localizationService.localize('template_queryBuilder'), + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'q' } + ] + } + ] + }; + } + //////////// + var service = { + getInsertDictionarySnippet: getInsertDictionarySnippet, + getInsertPartialSnippet: getInsertPartialSnippet, + getQuerySnippet: getQuerySnippet, + getRenderBodySnippet: getRenderBodySnippet, + getRenderSectionSnippet: getRenderSectionSnippet, + getAddSectionSnippet: getAddSectionSnippet, + getGeneralShortcuts: getGeneralShortcuts, + getEditorShortcuts: getEditorShortcuts, + getTemplateEditorShortcuts: getTemplateEditorShortcuts, + getPartialViewEditorShortcuts: getPartialViewEditorShortcuts + }; + return service; + } + angular.module('umbraco.services').factory('templateHelper', templateHelperService); + }()); + /** + * @ngdoc service + * @name umbraco.services.tinyMceService + * + * + * @description + * A service containing all logic for all of the Umbraco TinyMCE plugins + */ + function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) { + return { + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#configuration + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a collection of plugins available to the tinyMCE editor + * + */ + configuration: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('rteApiBaseUrl', 'GetConfiguration'), { cache: true }), 'Failed to retrieve tinymce configuration'); + }, + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#defaultPrevalues + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a default configration to fallback on in case none is provided + * + */ + defaultPrevalues: function () { + var cfg = {}; + cfg.toolbar = [ + 'code', + 'bold', + 'italic', + 'styleselect', + 'alignleft', + 'aligncenter', + 'alignright', + 'bullist', + 'numlist', + 'outdent', + 'indent', + 'link', + 'image', + 'umbmediapicker', + 'umbembeddialog', + 'umbmacro' + ]; + cfg.stylesheets = []; + cfg.dimensions = { height: 500 }; + cfg.maxImageSize = 500; + return cfg; + }, + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbrco insert embedded media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createInsertEmbeddedMedia: function (editor, scope, callback) { + editor.addButton('umbembeddialog', { + icon: 'custom icon-tv', + tooltip: 'Embed', + onclick: function () { + if (callback) { + callback(); + } + } + }); + }, + insertEmbeddedMediaInEditor: function (editor, preview) { + editor.insertContent(preview); + }, + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createMediaPicker + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbrco insert media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createMediaPicker: function (editor, scope, callback) { + editor.addButton('umbmediapicker', { + icon: 'custom icon-picture', + tooltip: 'Media Picker', + stateSelector: 'img', + onclick: function () { + var selectedElm = editor.selection.getNode(), currentTarget; + if (selectedElm.nodeName === 'IMG') { + var img = $(selectedElm); + var hasUdi = img.attr('data-udi') ? true : false; + currentTarget = { + altText: img.attr('alt'), + url: img.attr('src') + }; + if (hasUdi) { + currentTarget['udi'] = img.attr('data-udi'); + } else { + currentTarget['id'] = img.attr('rel'); + } + } + userService.getCurrentUser().then(function (userData) { + if (callback) { + callback(currentTarget, userData); + } + }); + } + }); + }, + insertMediaInEditor: function (editor, img) { + if (img) { + var hasUdi = img.udi ? true : false; + var data = { + alt: img.altText || '', + src: img.url ? img.url : 'nothing.jpg', + id: '__mcenew' + }; + if (hasUdi) { + data['data-udi'] = img.udi; + } else { + //Considering these fixed because UDI will now be used and thus + // we have no need for rel http://issues.umbraco.org/issue/U4-6228, http://issues.umbraco.org/issue/U4-6595 + data['rel'] = img.id; + data['data-id'] = img.id; + } + editor.insertContent(editor.dom.createHTML('img', data)); + $timeout(function () { + var imgElm = editor.dom.get('__mcenew'); + var size = editor.dom.getSize(imgElm); + if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { + var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); + var s = 'width: ' + newSize.width + 'px; height:' + newSize.height + 'px;'; + editor.dom.setAttrib(imgElm, 'style', s); + editor.dom.setAttrib(imgElm, 'id', null); + if (img.url) { + var src = img.url + '?width=' + newSize.width + '&height=' + newSize.height; + editor.dom.setAttrib(imgElm, 'data-mce-src', src); + } + } + }, 500); + } + }, + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createUmbracoMacro + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the insert umbrco macro tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createInsertMacro: function (editor, $scope, callback) { + var createInsertMacroScope = this; + /** Adds custom rules for the macro plugin and custom serialization */ + editor.on('preInit', function (args) { + //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out + editor.serializer.addRules('div'); + /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ + editor.serializer.addNodeFilter('div', function (nodes, name) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].attr('class') === 'umb-macro-holder' && nodes[i].parent && nodes[i].parent.name.toUpperCase() === 'P') { + nodes[i].parent.unwrap(); + } + } + }); + }); + /** + * Because the macro gets wrapped in a P tag because of the way 'enter' works, this + * method will return the macro element if not wrapped in a p, or the p if the macro + * element is the only one inside of it even if we are deep inside an element inside the macro + */ + function getRealMacroElem(element) { + var e = $(element).closest('.umb-macro-holder'); + if (e.length > 0) { + if (e.get(0).parentNode.nodeName === 'P') { + //now check if we're the only element + if (element.parentNode.childNodes.length === 1) { + return e.get(0).parentNode; + } + } + return e.get(0); + } + return null; + } + /** Adds the button instance */ + editor.addButton('umbmacro', { + icon: 'custom icon-settings-alt', + tooltip: 'Insert macro', + onPostRender: function () { + var ctrl = this; + var isOnMacroElement = false; + /** + if the selection comes from a different element that is not the macro's + we need to check if the selection includes part of the macro, if so we'll force the selection + to clear to the next element since if people can select part of the macro markup they can then modify it. + */ + function handleSelectionChange() { + if (!editor.selection.isCollapsed()) { + var endSelection = tinymce.activeEditor.selection.getEnd(); + var startSelection = tinymce.activeEditor.selection.getStart(); + //don't proceed if it's an entire element selected + if (endSelection !== startSelection) { + //if the end selection is a macro then move the cursor + //NOTE: we don't have to handle when the selection comes from a previous parent because + // that is automatically taken care of with the normal onNodeChanged logic since the + // evt.element will be the macro once it becomes part of the selection. + var $testForMacro = $(endSelection).closest('.umb-macro-holder'); + if ($testForMacro.length > 0) { + //it came from before so move after, if there is no after then select ourselves + var next = $testForMacro.next(); + if (next.length > 0) { + editor.selection.setCursorLocation($testForMacro.next().get(0)); + } else { + selectMacroElement($testForMacro.get(0)); + } + } + } + } + } + /** helper method to select the macro element */ + function selectMacroElement(macroElement) { + // move selection to top element to ensure we can't edit this + editor.selection.select(macroElement); + // check if the current selection *is* the element (ie bug) + var currentSelection = editor.selection.getStart(); + if (tinymce.isIE) { + if (!editor.dom.hasClass(currentSelection, 'umb-macro-holder')) { + while (!editor.dom.hasClass(currentSelection, 'umb-macro-holder') && currentSelection.parentNode) { + currentSelection = currentSelection.parentNode; + } + editor.selection.select(currentSelection); + } + } + } + /** + * Add a node change handler, test if we're editing a macro and select the whole thing, then set our isOnMacroElement flag. + * If we change the selection inside this method, then we end up in an infinite loop, so we have to remove ourselves + * from the event listener before changing selection, however, it seems that putting a break point in this method + * will always cause an 'infinite' loop as the caret keeps changing. + */ + function onNodeChanged(evt) { + //set our macro button active when on a node of class umb-macro-holder + var $macroElement = $(evt.element).closest('.umb-macro-holder'); + handleSelectionChange(); + //set the button active + ctrl.active($macroElement.length !== 0); + if ($macroElement.length > 0) { + var macroElement = $macroElement.get(0); + //remove the event listener before re-selecting + editor.off('NodeChange', onNodeChanged); + selectMacroElement(macroElement); + //set the flag + isOnMacroElement = true; + //re-add the event listener + editor.on('NodeChange', onNodeChanged); + } else { + isOnMacroElement = false; + } + } + /** when the contents load we need to find any macros declared and load in their content */ + editor.on('LoadContent', function (o) { + //get all macro divs and load their content + $(editor.dom.select('.umb-macro-holder.mceNonEditable')).each(function () { + createInsertMacroScope.loadMacroContent($(this), null, $scope); + }); + }); + /** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */ + editor.on('BeforeExecCommand', function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); + /** This double checks and ensures you can't paste content into the rendered macro */ + editor.on('Paste', function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); + //set onNodeChanged event listener + editor.on('NodeChange', onNodeChanged); + /** + * Listen for the keydown in the editor, we'll check if we are currently on a macro element, if so + * we'll check if the key down is a supported key which requires an action, otherwise we ignore the request + * so the macro cannot be edited. + */ + editor.on('KeyDown', function (e) { + if (isOnMacroElement) { + var macroElement = editor.selection.getNode(); + //get the 'real' element (either p or the real one) + macroElement = getRealMacroElem(macroElement); + //prevent editing + e.preventDefault(); + e.stopPropagation(); + var moveSibling = function (element, isNext) { + var $e = $(element); + var $sibling = isNext ? $e.next() : $e.prev(); + if ($sibling.length > 0) { + editor.selection.select($sibling.get(0)); + editor.selection.collapse(true); + } else { + //if we're moving previous and there is no sibling, then lets recurse and just select the next one + if (!isNext) { + moveSibling(element, true); + return; + } + //if there is no sibling we'll generate a new p at the end and select it + editor.setContent(editor.getContent() + '

     

    '); + editor.selection.select($(editor.dom.getRoot()).children().last().get(0)); + editor.selection.collapse(true); + } + }; + //supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left) + //supported keys to remove the macro (8-backspace, 46-delete) + //TODO: Should we make the enter key insert a line break before or leave it as moving to the next element? + if ($.inArray(e.keyCode, [ + 13, + 40, + 39 + ]) !== -1) { + //move to next element + moveSibling(macroElement, true); + } else if ($.inArray(e.keyCode, [ + 27, + 38, + 37 + ]) !== -1) { + //move to prev element + moveSibling(macroElement, false); + } else if ($.inArray(e.keyCode, [ + 8, + 46 + ]) !== -1) { + //delete macro element + //move first, then delete + moveSibling(macroElement, false); + editor.dom.remove(macroElement); + } + return; + } + }); + }, + /** The insert macro button click event handler */ + onclick: function () { + var dialogData = { + //flag for use in rte so we only show macros flagged for the editor + richTextEditor: true + }; + //when we click we could have a macro already selected and in that case we'll want to edit the current parameters + //so we'll need to extract them and submit them to the dialog. + var macroElement = editor.selection.getNode(); + macroElement = getRealMacroElem(macroElement); + if (macroElement) { + //we have a macro selected so we'll need to parse it's alias and parameters + var contents = $(macroElement).contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw 'Cannot parse the current macro, the syntax in the editor is invalid'; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + dialogData = { macroData: parsed }; + } + if (callback) { + callback(dialogData); + } + } + }); + }, + insertMacroInEditor: function (editor, macroObject, $scope) { + //put the macro syntax in comments, we will parse this out on the server side to be used + //for persisting. + var macroSyntaxComment = ''; + //create an id class for this element so we can re-select it after inserting + var uniqueId = 'umb-macro-' + editor.dom.uniqueId(); + var macroDiv = editor.dom.create('div', { 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId }, macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); + editor.selection.setNode(macroDiv); + var $macroDiv = $(editor.dom.select('div.umb-macro-holder.' + uniqueId)); + //async load the macro content + this.loadMacroContent($macroDiv, macroObject, $scope); + }, + /** loads in the macro content async from the server */ + loadMacroContent: function ($macroDiv, macroData, $scope) { + //if we don't have the macroData, then we'll need to parse it from the macro div + if (!macroData) { + var contents = $macroDiv.contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw 'Cannot parse the current macro, the syntax in the editor is invalid'; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + macroData = parsed; + } + var $ins = $macroDiv.find('ins'); + //show the throbber + $macroDiv.addClass('loading'); + var contentId = $routeParams.id; + //need to wrap in safe apply since this might be occuring outside of angular + angularHelper.safeApply($scope, function () { + macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary).then(function (htmlResult) { + $macroDiv.removeClass('loading'); + htmlResult = htmlResult.trim(); + if (htmlResult !== '') { + $ins.html(htmlResult); + } + }); + }); + }, + createLinkPicker: function (editor, $scope, onClick) { + function createLinkList(callback) { + return function () { + var linkList = editor.settings.link_list; + if (typeof linkList === 'string') { + tinymce.util.XHR.send({ + url: linkList, + success: function (text) { + callback(tinymce.util.JSON.parse(text)); + } + }); + } else { + callback(linkList); + } + }; + } + function showDialog(linkList) { + var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText; + var win, linkListCtrl, relListCtrl, targetListCtrl; + function linkListChangeHandler(e) { + var textCtrl = win.find('#text'); + if (!textCtrl.value() || e.lastControl && textCtrl.value() === e.lastControl.text()) { + textCtrl.value(e.control.text()); + } + win.find('#href').value(e.control.value()); + } + function buildLinkList() { + var linkListItems = [{ + text: 'None', + value: '' + }]; + tinymce.each(linkList, function (link) { + linkListItems.push({ + text: link.text || link.title, + value: link.value || link.url, + menu: link.menu + }); + }); + return linkListItems; + } + function buildRelList(relValue) { + var relListItems = [{ + text: 'None', + value: '' + }]; + tinymce.each(editor.settings.rel_list, function (rel) { + relListItems.push({ + text: rel.text || rel.title, + value: rel.value, + selected: relValue === rel.value + }); + }); + return relListItems; + } + function buildTargetList(targetValue) { + var targetListItems = [{ + text: 'None', + value: '' + }]; + if (!editor.settings.target_list) { + targetListItems.push({ + text: 'New window', + value: '_blank' + }); + } + tinymce.each(editor.settings.target_list, function (target) { + targetListItems.push({ + text: target.text || target.title, + value: target.value, + selected: targetValue === target.value + }); + }); + return targetListItems; + } + function buildAnchorListControl(url) { + var anchorList = []; + tinymce.each(editor.dom.select('a:not([href])'), function (anchor) { + var id = anchor.name || anchor.id; + if (id) { + anchorList.push({ + text: id, + value: '#' + id, + selected: url.indexOf('#' + id) !== -1 + }); + } + }); + if (anchorList.length) { + anchorList.unshift({ + text: 'None', + value: '' + }); + return { + name: 'anchor', + type: 'listbox', + label: 'Anchors', + values: anchorList, + onselect: linkListChangeHandler + }; + } + } + function updateText() { + if (!initialText && data.text.length === 0) { + this.parent().parent().find('#text')[0].value(this.value()); + } + } + selectedElm = selection.getNode(); + anchorElm = dom.getParent(selectedElm, 'a[href]'); + data.text = initialText = anchorElm ? anchorElm.innerText || anchorElm.textContent : selection.getContent({ format: 'text' }); + data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; + data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; + data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; + if (selectedElm.nodeName === 'IMG') { + data.text = initialText = ' '; + } + if (linkList) { + linkListCtrl = { + type: 'listbox', + label: 'Link list', + values: buildLinkList(), + onselect: linkListChangeHandler + }; + } + if (editor.settings.target_list !== false) { + targetListCtrl = { + name: 'target', + type: 'listbox', + label: 'Target', + values: buildTargetList(data.target) + }; + } + if (editor.settings.rel_list) { + relListCtrl = { + name: 'rel', + type: 'listbox', + label: 'Rel', + values: buildRelList(data.rel) + }; + } + var injector = angular.element(document.getElementById('umbracoMainPageBody')).injector(); + var dialogService = injector.get('dialogService'); + var currentTarget = null; + //if we already have a link selected, we want to pass that data over to the dialog + if (anchorElm) { + var anchor = $(anchorElm); + currentTarget = { + name: anchor.attr('title'), + url: anchor.attr('href'), + target: anchor.attr('target') + }; + //locallink detection, we do this here, to avoid poluting the dialogservice + //so the dialog service can just expect to get a node-like structure + if (currentTarget.url.indexOf('localLink:') > 0) { + var linkId = currentTarget.url.substring(currentTarget.url.indexOf(':') + 1, currentTarget.url.length - 1); + //we need to check if this is an INT or a UDI + var parsedIntId = parseInt(linkId, 10); + if (isNaN(parsedIntId)) { + //it's a UDI + currentTarget.udi = linkId; + } else { + currentTarget.id = linkId; + } + } + } + if (onClick) { + onClick(currentTarget, anchorElm); + } + } + editor.addButton('link', { + icon: 'link', + tooltip: 'Insert/edit link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]' + }); + editor.addButton('unlink', { + icon: 'unlink', + tooltip: 'Remove link', + cmd: 'unlink', + stateSelector: 'a[href]' + }); + editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); + this.showDialog = showDialog; + editor.addMenuItem('link', { + icon: 'link', + text: 'Insert link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]', + context: 'insert', + prependToContext: true + }); + }, + insertLinkInEditor: function (editor, target, anchorElm) { + var href = target.url; + // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null + var hasUdi = target.udi ? true : false; + var id = hasUdi ? target.udi : target.id ? target.id : null; + //Create a json obj used to create the attributes for the tag + function createElemAttributes() { + var a = { + href: href, + title: target.name, + target: target.target ? target.target : null, + rel: target.rel ? target.rel : null + }; + if (hasUdi) { + a['data-udi'] = target.udi; + } else if (target.id) { + a['data-id'] = target.id; + } + return a; + } + function insertLink() { + if (anchorElm) { + editor.dom.setAttribs(anchorElm, createElemAttributes()); + editor.selection.select(anchorElm); + editor.execCommand('mceEndTyping'); + } else { + editor.execCommand('mceInsertLink', false, createElemAttributes()); + } + } + if (!href) { + editor.execCommand('unlink'); + return; + } + //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set + if (id && (angular.isUndefined(target.isMedia) || !target.isMedia)) { + href = '/{localLink:' + id + '}'; + insertLink(); + return; + } + // Is email and not //user@domain.com + if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) { + href = 'mailto:' + href; + insertLink(); + return; + } + // Is www. prefixed + if (/^\s*www\./i.test(href)) { + href = 'http://' + href; + insertLink(); + return; + } + insertLink(); + } + }; + } + angular.module('umbraco.services').factory('tinyMceService', tinyMceService); + /** + * @ngdoc service + * @name umbraco.services.treeService + * @function + * + * @description + * The tree service factory, used internally by the umbTree and umbTreeItem directives + */ + function treeService($q, treeResource, iconHelper, notificationsService, eventsService) { + //SD: Have looked at putting this in sessionStorage (not localStorage since that means you wouldn't be able to work + // in multiple tabs) - however our tree structure is cyclical, meaning a node has a reference to it's parent and it's children + // which you cannot serialize to sessionStorage. There's really no benefit of session storage except that you could refresh + // a tab and have the trees where they used to be - supposed that is kind of nice but would mean we'd have to store the parent + // as a nodeid reference instead of a variable with a getParent() method. + var treeCache = {}; + var standardCssClass = 'icon umb-tree-icon sprTree'; + function getCacheKey(args) { + //if there is no cache key they return null - it won't be cached. + if (!args || !args.cacheKey) { + return null; + } + var cacheKey = args.cacheKey; + cacheKey += '_' + args.section; + return cacheKey; + } + return { + /** Internal method to return the tree cache */ + _getTreeCache: function () { + return treeCache; + }, + /** Internal method that ensures there's a routePath, parent and level property on each tree node and adds some icon specific properties so that the nodes display properly */ + _formatNodeDataForUseInUI: function (parentNode, treeNodes, section, level) { + //if no level is set, then we make it 1 + var childLevel = level ? level : 1; + //set the section if it's not already set + if (!parentNode.section) { + parentNode.section = section; + } + if (parentNode.metaData && parentNode.metaData.noAccess === true) { + if (!parentNode.cssClasses) { + parentNode.cssClasses = []; + } + parentNode.cssClasses.push('no-access'); + } + //create a method outside of the loop to return the parent - otherwise jshint blows up + var funcParent = function () { + return parentNode; + }; + for (var i = 0; i < treeNodes.length; i++) { + var treeNode = treeNodes[i]; + treeNode.level = childLevel; + //create a function to get the parent node, we could assign the parent node but + // then we cannot serialize this entity because we have a cyclical reference. + // Instead we just make a function to return the parentNode. + treeNode.parent = funcParent; + //set the section for each tree node - this allows us to reference this easily when accessing tree nodes + treeNode.section = section; + //if there is not route path specified, then set it automatically, + //if this is a tree root node then we want to route to the section's dashboard + if (!treeNode.routePath) { + if (treeNode.metaData && treeNode.metaData['treeAlias']) { + //this is a root node + treeNode.routePath = section; + } else { + var treeAlias = this.getTreeAlias(treeNode); + treeNode.routePath = section + '/' + treeAlias + '/edit/' + treeNode.id; + } + } + //now, format the icon data + if (treeNode.iconIsClass === undefined || treeNode.iconIsClass) { + var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNode); + treeNode.cssClass = standardCssClass + ' ' + converted; + if (converted.startsWith('.')) { + //its legacy so add some width/height + treeNode.style = 'height:16px;width:16px;'; + } else { + treeNode.style = ''; + } + } else { + treeNode.style = 'background-image: url(\'' + treeNode.iconFilePath + '\');'; + //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this + treeNode.cssClass = standardCssClass + ' legacy-custom-file'; + } + if (treeNode.metaData && treeNode.metaData.noAccess === true) { + if (!treeNode.cssClasses) { + treeNode.cssClasses = []; + } + treeNode.cssClasses.push('no-access'); + } + } + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#getTreePackageFolder + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Determines if the current tree is a plugin tree and if so returns the package folder it has declared + * so we know where to find it's views, otherwise it will just return undefined. + * + * @param {String} treeAlias The tree alias to check + */ + getTreePackageFolder: function (treeAlias) { + //we determine this based on the server variables + if (Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.trees && angular.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { + var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) { + return item.alias === treeAlias; + }); + return found ? found.packageFolder : undefined; + } + return undefined; + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#clearCache + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Clears the tree cache - with optional cacheKey, optional section or optional filter. + * + * @param {Object} args arguments + * @param {String} args.cacheKey optional cachekey - this is used to clear specific trees in dialogs + * @param {String} args.section optional section alias - clear tree for a given section + * @param {String} args.childrenOf optional parent ID - only clear the cache below a specific node + */ + clearCache: function (args) { + //clear all if not specified + if (!args) { + treeCache = {}; + } else { + //if section and cache key specified just clear that cache + if (args.section && args.cacheKey) { + var cacheKey = getCacheKey(args); + if (cacheKey && treeCache && treeCache[cacheKey] != null) { + treeCache = _.omit(treeCache, cacheKey); + } + } else if (args.childrenOf) { + //if childrenOf is supplied a cacheKey must be supplied as well + if (!args.cacheKey) { + throw 'args.cacheKey is required if args.childrenOf is supplied'; + } + //this will clear out all children for the parentId passed in to this parameter, we'll + // do this by recursing and specifying a filter + var self = this; + this.clearCache({ + cacheKey: args.cacheKey, + filter: function (cc) { + //get the new parent node from the tree cache + var parent = self.getDescendantNode(cc.root, args.childrenOf); + if (parent) { + //clear it's children and set to not expanded + parent.children = null; + parent.expanded = false; + } + //return the cache to be saved + return cc; + } + }); + } else if (args.filter && angular.isFunction(args.filter)) { + //if a filter is supplied a cacheKey must be supplied as well + if (!args.cacheKey) { + throw 'args.cacheKey is required if args.filter is supplied'; + } + //if a filter is supplied the function needs to return the data to keep + var byKey = treeCache[args.cacheKey]; + if (byKey) { + var result = args.filter(byKey); + if (result) { + //set the result to the filtered data + treeCache[args.cacheKey] = result; + } else { + //remove the cache + treeCache = _.omit(treeCache, args.cacheKey); + } + } + } else if (args.cacheKey) { + //if only the cache key is specified, then clear all cache starting with that key + var allKeys1 = _.keys(treeCache); + var toRemove1 = _.filter(allKeys1, function (k) { + return k.startsWith(args.cacheKey + '_'); + }); + treeCache = _.omit(treeCache, toRemove1); + } else if (args.section) { + //if only the section is specified then clear all cache regardless of cache key by that section + var allKeys2 = _.keys(treeCache); + var toRemove2 = _.filter(allKeys2, function (k) { + return k.endsWith('_' + args.section); + }); + treeCache = _.omit(treeCache, toRemove2); + } + } + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#loadNodeChildren + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Clears all node children, gets it's up-to-date children from the server and re-assigns them and then + * returns them in a promise. + * @param {object} args An arguments object + * @param {object} args.node The tree node + * @param {object} args.section The current section + */ + loadNodeChildren: function (args) { + if (!args) { + throw 'No args object defined for loadNodeChildren'; + } + if (!args.node) { + throw 'No node defined on args object for loadNodeChildren'; + } + this.removeChildNodes(args.node); + args.node.loading = true; + return this.getChildren(args).then(function (data) { + //set state to done and expand (only if there actually are children!) + args.node.loading = false; + args.node.children = data; + if (args.node.children && args.node.children.length > 0) { + args.node.expanded = true; + args.node.hasChildren = true; + } + return data; + }, function (reason) { + //in case of error, emit event + eventsService.emit('treeService.treeNodeLoadError', { error: reason }); + //stop show the loading indicator + args.node.loading = false; + //tell notications about the error + notificationsService.error(reason); + return reason; + }); + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#removeNode + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Removes a given node from the tree + * @param {object} treeNode the node to remove + */ + removeNode: function (treeNode) { + if (!angular.isFunction(treeNode.parent)) { + return; + } + if (treeNode.parent() == null) { + throw 'Cannot remove a node that doesn\'t have a parent'; + } + //remove the current item from it's siblings + treeNode.parent().children.splice(treeNode.parent().children.indexOf(treeNode), 1); + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#removeChildNodes + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Removes all child nodes from a given tree node + * @param {object} treeNode the node to remove children from + */ + removeChildNodes: function (treeNode) { + treeNode.expanded = false; + treeNode.children = []; + treeNode.hasChildren = false; + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#getChildNode + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Gets a child node with a given ID, from a specific treeNode + * @param {object} treeNode to retrive child node from + * @param {int} id id of child node + */ + getChildNode: function (treeNode, id) { + if (!treeNode.children) { + return null; + } + var found = _.find(treeNode.children, function (child) { + return String(child.id).toLowerCase() === String(id).toLowerCase(); + }); + return found === undefined ? null : found; + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#getDescendantNode + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Gets a descendant node by id + * @param {object} treeNode to retrive descendant node from + * @param {int} id id of descendant node + * @param {string} treeAlias - optional tree alias, if fetching descendant node from a child of a listview document + */ + getDescendantNode: function (treeNode, id, treeAlias) { + //validate if it is a section container since we'll need a treeAlias if it is one + if (treeNode.isContainer === true && !treeAlias) { + throw 'Cannot get a descendant node from a section container node without a treeAlias specified'; + } + //if it is a section container, we need to find the tree to be searched + if (treeNode.isContainer) { + var foundRoot = null; + for (var c = 0; c < treeNode.children.length; c++) { + if (this.getTreeAlias(treeNode.children[c]) === treeAlias) { + foundRoot = treeNode.children[c]; + break; + } + } + if (!foundRoot) { + throw 'Could not find a tree in the current section with alias ' + treeAlias; + } + treeNode = foundRoot; + } + //check this node + if (treeNode.id === id) { + return treeNode; + } + //check the first level + var found = this.getChildNode(treeNode, id); + if (found) { + return found; + } + //check each child of this node + if (!treeNode.children) { + return null; + } + for (var i = 0; i < treeNode.children.length; i++) { + var child = treeNode.children[i]; + if (child.children && angular.isArray(child.children) && child.children.length > 0) { + //recurse + found = this.getDescendantNode(child, id); + if (found) { + return found; + } + } + } + //not found + return found === undefined ? null : found; + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#getTreeRoot + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Gets the root node of the current tree type for a given tree node + * @param {object} treeNode to retrive tree root node from + */ + getTreeRoot: function (treeNode) { + if (!treeNode) { + throw 'treeNode cannot be null'; + } + //all root nodes have metadata key 'treeAlias' + var root = null; + var current = treeNode; + while (root === null && current) { + if (current.metaData && current.metaData['treeAlias']) { + root = current; + } else if (angular.isFunction(current.parent)) { + //we can only continue if there is a parent() method which means this + // tree node was loaded in as part of a real tree, not just as a single tree + // node from the server. + current = current.parent(); + } else { + current = null; + } + } + return root; + }, + /** Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node */ + /** + * @ngdoc method + * @name umbraco.services.treeService#getTreeAlias + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node + * @param {object} treeNode to retrive tree alias from + */ + getTreeAlias: function (treeNode) { + var root = this.getTreeRoot(treeNode); + if (root) { + return root.metaData['treeAlias']; + } + return ''; + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#getTree + * @methodOf umbraco.services.treeService + * @function + * + * @description + * gets the tree, returns a promise + * @param {object} args Arguments + * @param {string} args.section Section alias + * @param {string} args.cacheKey Optional cachekey + */ + getTree: function (args) { + var deferred = $q.defer(); + //set defaults + if (!args) { + args = { + section: 'content', + cacheKey: null + }; + } else if (!args.section) { + args.section = 'content'; + } + var cacheKey = getCacheKey(args); + //return the cache if it exists + if (cacheKey && treeCache[cacheKey] !== undefined) { + deferred.resolve(treeCache[cacheKey]); + return deferred.promise; + } + var self = this; + treeResource.loadApplication(args).then(function (data) { + //this will be called once the tree app data has loaded + var result = { + name: data.name, + alias: args.section, + root: data + }; + //we need to format/modify some of the node data to be used in our app. + self._formatNodeDataForUseInUI(result.root, result.root.children, args.section); + //cache this result if a cache key is specified - generally a cache key should ONLY + // be specified for application trees, dialog trees should not be cached. + if (cacheKey) { + treeCache[cacheKey] = result; + deferred.resolve(treeCache[cacheKey]); + } + //return un-cached + deferred.resolve(result); + }); + return deferred.promise; + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#getMenu + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Returns available menu actions for a given tree node + * @param {object} args Arguments + * @param {string} args.treeNode tree node object to retrieve the menu for + */ + getMenu: function (args) { + if (!args) { + throw 'args cannot be null'; + } + if (!args.treeNode) { + throw 'args.treeNode cannot be null'; + } + return treeResource.loadMenu(args.treeNode).then(function (data) { + //need to convert the icons to new ones + for (var i = 0; i < data.length; i++) { + data[i].cssclass = iconHelper.convertFromLegacyIcon(data[i].cssclass); + } + return data; + }); + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#getChildren + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Gets the children from the server for a given node + * @param {object} args Arguments + * @param {object} args.node tree node object to retrieve the children for + * @param {string} args.section current section alias + */ + getChildren: function (args) { + if (!args) { + throw 'No args object defined for getChildren'; + } + if (!args.node) { + throw 'No node defined on args object for getChildren'; + } + var section = args.section || 'content'; + var treeItem = args.node; + var self = this; + return treeResource.loadNodes({ node: treeItem }).then(function (data) { + //now that we have the data, we need to add the level property to each item and the view + self._formatNodeDataForUseInUI(treeItem, data, section, treeItem.level + 1); + return data; + }); + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#reloadNode + * @methodOf umbraco.services.treeService + * @function + * + * @description + * Re-loads the single node from the server + * @param {object} node Tree node to reload + */ + reloadNode: function (node) { + if (!node) { + throw 'node cannot be null'; + } + if (!node.parent()) { + throw 'cannot reload a single node without a parent'; + } + if (!node.section) { + throw 'cannot reload a single node without an assigned node.section'; + } + var deferred = $q.defer(); + //set the node to loading + node.loading = true; + this.getChildren({ + node: node.parent(), + section: node.section + }).then(function (data) { + //ok, now that we have the children, find the node we're reloading + var found = _.find(data, function (item) { + return item.id === node.id; + }); + if (found) { + //now we need to find the node in the parent.children collection to replace + var index = _.indexOf(node.parent().children, node); + //the trick here is to not actually replace the node - this would cause the delete animations + //to fire, instead we're just going to replace all the properties of this node. + //there should always be a method assigned but we'll check anyways + if (angular.isFunction(node.parent().children[index].updateNodeData)) { + node.parent().children[index].updateNodeData(found); + } else { + //just update as per normal - this means styles, etc.. won't be applied + _.extend(node.parent().children[index], found); + } + //set the node loading + node.parent().children[index].loading = false; + //return + deferred.resolve(node.parent().children[index]); + } else { + deferred.reject(); + } + }, function () { + deferred.reject(); + }); + return deferred.promise; + }, + /** + * @ngdoc method + * @name umbraco.services.treeService#getPath + * @methodOf umbraco.services.treeService + * @function + * + * @description + * This will return the current node's path by walking up the tree + * @param {object} node Tree node to retrieve path for + */ + getPath: function (node) { + if (!node) { + throw 'node cannot be null'; + } + if (!angular.isFunction(node.parent)) { + throw 'node.parent is not a function, the path cannot be resolved'; + } + //all root nodes have metadata key 'treeAlias' + var reversePath = []; + var current = node; + while (current != null) { + reversePath.push(current.id); + if (current.metaData && current.metaData['treeAlias']) { + current = null; + } else { + current = current.parent(); + } + } + return reversePath.reverse(); + }, + syncTree: function (args) { + if (!args) { + throw 'No args object defined for syncTree'; + } + if (!args.node) { + throw 'No node defined on args object for syncTree'; + } + if (!args.path) { + throw 'No path defined on args object for syncTree'; + } + if (!angular.isArray(args.path)) { + throw 'Path must be an array'; + } + if (args.path.length < 1) { + //if there is no path, make -1 the path, and that should sync the tree root + args.path.push('-1'); + } + var deferred = $q.defer(); + //get the rootNode for the current node, we'll sync based on that + var root = this.getTreeRoot(args.node); + if (!root) { + throw 'Could not get the root tree node based on the node passed in'; + } + //now we want to loop through the ids in the path, first we'll check if the first part + //of the path is the root node, otherwise we'll search it's children. + var currPathIndex = 0; + //if the first id is the root node and there's only one... then consider it synced + if (String(args.path[currPathIndex]).toLowerCase() === String(args.node.id).toLowerCase()) { + if (args.path.length === 1) { + //return the root + deferred.resolve(root); + return deferred.promise; + } else { + //move to the next path part and continue + currPathIndex = 1; + } + } + //now that we have the first id to lookup, we can start the process + var self = this; + var node = args.node; + var doSync = function () { + //check if it exists in the already loaded children + var child = self.getChildNode(node, args.path[currPathIndex]); + if (child) { + if (args.path.length === currPathIndex + 1) { + //woot! synced the node + if (!args.forceReload) { + deferred.resolve(child); + } else { + //even though we've found the node if forceReload is specified + //we want to go update this single node from the server + self.reloadNode(child).then(function (reloaded) { + deferred.resolve(reloaded); + }, function () { + deferred.reject(); + }); + } + } else { + //now we need to recurse with the updated node/currPathIndex + currPathIndex++; + node = child; + //recurse + doSync(); + } + } else { + //couldn't find it in the + self.loadNodeChildren({ + node: node, + section: node.section + }).then(function () { + //ok, got the children, let's find it + var found = self.getChildNode(node, args.path[currPathIndex]); + if (found) { + if (args.path.length === currPathIndex + 1) { + //woot! synced the node + deferred.resolve(found); + } else { + //now we need to recurse with the updated node/currPathIndex + currPathIndex++; + node = found; + //recurse + doSync(); + } + } else { + //fail! + deferred.reject(); + } + }, function () { + //fail! + deferred.reject(); + }); + } + }; + //start + doSync(); + return deferred.promise; + } + }; + } + angular.module('umbraco.services').factory('treeService', treeService); + (function () { + 'use strict'; + /** + * @ngdoc service + * @name umbraco.services.umbDataFormatter + * @description A helper object used to format/transform JSON Umbraco data, mostly used for persisting data to the server + **/ + function umbDataFormatter() { + return { + formatChangePasswordModel: function (model) { + if (!model) { + return null; + } + var trimmed = _.omit(model, [ + 'confirm', + 'generatedPassword' + ]); + //ensure that the pass value is null if all child properties are null + var allNull = true; + var vals = _.values(trimmed); + for (var k = 0; k < vals.length; k++) { + if (vals[k] !== null && vals[k] !== undefined) { + allNull = false; + } + } + if (allNull) { + return null; + } + return trimmed; + }, + formatContentTypePostData: function (displayModel, action) { + //create the save model from the display model + var saveModel = _.pick(displayModel, 'compositeContentTypes', 'isContainer', 'allowAsRoot', 'allowedTemplates', 'allowedContentTypes', 'alias', 'description', 'thumbnail', 'name', 'id', 'icon', 'trashed', 'key', 'parentId', 'alias', 'path'); + //TODO: Map these + saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { + return t.alias; + }); + saveModel.defaultTemplate = displayModel.defaultTemplate ? displayModel.defaultTemplate.alias : null; + var realGroups = _.reject(displayModel.groups, function (g) { + //do not include these tabs + return g.tabState === 'init'; + }); + saveModel.groups = _.map(realGroups, function (g) { + var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name'); + var realProperties = _.reject(g.properties, function (p) { + //do not include these properties + return p.propertyState === 'init' || p.inherited === true; + }); + var saveProperties = _.map(realProperties, function (p) { + var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile'); + return saveProperty; + }); + saveGroup.properties = saveProperties; + //if this is an inherited group and there are not non-inherited properties on it, then don't send up the data + if (saveGroup.inherited === true && saveProperties.length === 0) { + return null; + } + return saveGroup; + }); + //we don't want any null groups + saveModel.groups = _.reject(saveModel.groups, function (g) { + return !g; + }); + return saveModel; + }, + /** formats the display model used to display the data type to the model used to save the data type */ + formatDataTypePostData: function (displayModel, preValues, action) { + var saveModel = { + parentId: displayModel.parentId, + id: displayModel.id, + name: displayModel.name, + selectedEditor: displayModel.selectedEditor, + //set the action on the save model + action: action, + preValues: [] + }; + for (var i = 0; i < preValues.length; i++) { + saveModel.preValues.push({ + key: preValues[i].alias, + value: preValues[i].value + }); + } + return saveModel; + }, + /** formats the display model used to display the user to the model used to save the user */ + formatUserPostData: function (displayModel) { + //create the save model from the display model + var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message', 'changePassword'); + saveModel.changePassword = this.formatChangePasswordModel(saveModel.changePassword); + //make sure the userGroups are just a string array + var currGroups = saveModel.userGroups; + var formattedGroups = []; + for (var i = 0; i < currGroups.length; i++) { + if (!angular.isString(currGroups[i])) { + formattedGroups.push(currGroups[i].alias); + } else { + formattedGroups.push(currGroups[i]); + } + } + saveModel.userGroups = formattedGroups; + //make sure the startnodes are just a string array + var props = [ + 'startContentIds', + 'startMediaIds' + ]; + for (var m = 0; m < props.length; m++) { + var startIds = saveModel[props[m]]; + if (!startIds) { + continue; + } + var formattedIds = []; + for (var j = 0; j < startIds.length; j++) { + formattedIds.push(Number(startIds[j].id)); + } + saveModel[props[m]] = formattedIds; + } + return saveModel; + }, + /** formats the display model used to display the user group to the model used to save the user group*/ + formatUserGroupPostData: function (displayModel, action) { + //create the save model from the display model + var saveModel = _.pick(displayModel, 'id', 'alias', 'name', 'icon', 'sections', 'users', 'defaultPermissions', 'assignedPermissions'); + // the start nodes cannot be picked as the property name needs to change - assign manually + saveModel.startContentId = displayModel['contentStartNode']; + saveModel.startMediaId = displayModel['mediaStartNode']; + //set the action on the save model + saveModel.action = action; + if (!saveModel.id) { + saveModel.id = 0; + } + //the permissions need to just be the array of permission letters, currently it will be a dictionary of an array + var currDefaultPermissions = saveModel.defaultPermissions; + var saveDefaultPermissions = []; + _.each(currDefaultPermissions, function (value, key, list) { + _.each(value, function (element, index, list) { + if (element.checked) { + saveDefaultPermissions.push(element.permissionCode); + } + }); + }); + saveModel.defaultPermissions = saveDefaultPermissions; + //now format that assigned/content permissions + var currAssignedPermissions = saveModel.assignedPermissions; + var saveAssignedPermissions = {}; + _.each(currAssignedPermissions, function (nodePermissions, index) { + saveAssignedPermissions[nodePermissions.id] = []; + _.each(nodePermissions.allowedPermissions, function (permission, index) { + if (permission.checked) { + saveAssignedPermissions[nodePermissions.id].push(permission.permissionCode); + } + }); + }); + saveModel.assignedPermissions = saveAssignedPermissions; + //make sure the sections are just a string array + var currSections = saveModel.sections; + var formattedSections = []; + for (var i = 0; i < currSections.length; i++) { + if (!angular.isString(currSections[i])) { + formattedSections.push(currSections[i].alias); + } else { + formattedSections.push(currSections[i]); + } + } + saveModel.sections = formattedSections; + //make sure the user are just an int array + var currUsers = saveModel.users; + var formattedUsers = []; + for (var j = 0; j < currUsers.length; j++) { + if (!angular.isNumber(currUsers[j])) { + formattedUsers.push(currUsers[j].id); + } else { + formattedUsers.push(currUsers[j]); + } + } + saveModel.users = formattedUsers; + //make sure the startnodes are just an int if one is set + var props = [ + 'startContentId', + 'startMediaId' + ]; + for (var m = 0; m < props.length; m++) { + var startId = saveModel[props[m]]; + if (!startId) { + continue; + } + saveModel[props[m]] = startId.id; + } + saveModel.parentId = -1; + return saveModel; + }, + /** formats the display model used to display the member to the model used to save the member */ + formatMemberPostData: function (displayModel, action) { + //this is basically the same as for media but we need to explicitly add the username,email, password to the save model + var saveModel = this.formatMediaPostData(displayModel, action); + saveModel.key = displayModel.key; + var genericTab = _.find(displayModel.tabs, function (item) { + return item.id === 0; + }); + //map the member login, email, password and groups + var propLogin = _.find(genericTab.properties, function (item) { + return item.alias === '_umb_login'; + }); + var propEmail = _.find(genericTab.properties, function (item) { + return item.alias === '_umb_email'; + }); + var propPass = _.find(genericTab.properties, function (item) { + return item.alias === '_umb_password'; + }); + var propGroups = _.find(genericTab.properties, function (item) { + return item.alias === '_umb_membergroup'; + }); + saveModel.email = propEmail.value.trim(); + saveModel.username = propLogin.value.trim(); + saveModel.password = this.formatChangePasswordModel(propPass.value); + var selectedGroups = []; + for (var n in propGroups.value) { + if (propGroups.value[n] === true) { + selectedGroups.push(n); + } + } + saveModel.memberGroups = selectedGroups; + //turn the dictionary into an array of pairs + var memberProviderPropAliases = _.pairs(displayModel.fieldConfig); + _.each(displayModel.tabs, function (tab) { + _.each(tab.properties, function (prop) { + var foundAlias = _.find(memberProviderPropAliases, function (item) { + return prop.alias === item[1]; + }); + if (foundAlias) { + //we know the current property matches an alias, now we need to determine which membership provider property it was for + // by looking at the key + switch (foundAlias[0]) { + case 'umbracoMemberLockedOut': + saveModel.isLockedOut = prop.value.toString() === '1' ? true : false; + break; + case 'umbracoMemberApproved': + saveModel.isApproved = prop.value.toString() === '1' ? true : false; + break; + case 'umbracoMemberComments': + saveModel.comments = prop.value; + break; + } + } + }); + }); + return saveModel; + }, + /** formats the display model used to display the media to the model used to save the media */ + formatMediaPostData: function (displayModel, action) { + //NOTE: the display model inherits from the save model so we can in theory just post up the display model but + // we don't want to post all of the data as it is unecessary. + var saveModel = { + id: displayModel.id, + properties: [], + name: displayModel.name, + contentTypeAlias: displayModel.contentTypeAlias, + parentId: displayModel.parentId, + //set the action on the save model + action: action + }; + _.each(displayModel.tabs, function (tab) { + _.each(tab.properties, function (prop) { + //don't include the custom generic tab properties + if (!prop.alias.startsWith('_umb_')) { + saveModel.properties.push({ + id: prop.id, + alias: prop.alias, + value: prop.value + }); + } + }); + }); + return saveModel; + }, + /** formats the display model used to display the content to the model used to save the content */ + formatContentPostData: function (displayModel, action) { + //this is basically the same as for media but we need to explicitly add some extra properties + var saveModel = this.formatMediaPostData(displayModel, action); + var genericTab = _.find(displayModel.tabs, function (item) { + return item.id === 0; + }); + var propExpireDate = _.find(genericTab.properties, function (item) { + return item.alias === '_umb_expiredate'; + }); + var propReleaseDate = _.find(genericTab.properties, function (item) { + return item.alias === '_umb_releasedate'; + }); + var propTemplate = _.find(genericTab.properties, function (item) { + return item.alias === '_umb_template'; + }); + saveModel.expireDate = propExpireDate ? propExpireDate.value : null; + saveModel.releaseDate = propReleaseDate ? propReleaseDate.value : null; + saveModel.templateAlias = propTemplate ? propTemplate.value : null; + return saveModel; + } + }; + } + angular.module('umbraco.services').factory('umbDataFormatter', umbDataFormatter); + }()); + /** +* @ngdoc service +* @name umbraco.services.umbRequestHelper +* @description A helper object used for sending requests to the server +**/ + function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService, eventsService) { + return { + /** + * @ngdoc method + * @name umbraco.services.umbRequestHelper#convertVirtualToAbsolutePath + * @methodOf umbraco.services.umbRequestHelper + * @function + * + * @description + * This will convert a virtual path (i.e. ~/App_Plugins/Blah/Test.html ) to an absolute path + * + * @param {string} a virtual path, if this is already an absolute path it will just be returned, if this is a relative path an exception will be thrown + */ + convertVirtualToAbsolutePath: function (virtualPath) { + if (virtualPath.startsWith('/')) { + return virtualPath; + } + if (!virtualPath.startsWith('~/')) { + throw 'The path ' + virtualPath + ' is not a virtual path'; + } + if (!Umbraco.Sys.ServerVariables.application.applicationPath) { + throw 'No applicationPath defined in Umbraco.ServerVariables.application.applicationPath'; + } + return Umbraco.Sys.ServerVariables.application.applicationPath + virtualPath.trimStart('~/'); + }, + /** + * @ngdoc method + * @name umbraco.services.umbRequestHelper#dictionaryToQueryString + * @methodOf umbraco.services.umbRequestHelper + * @function + * + * @description + * This will turn an array of key/value pairs or a standard dictionary into a query string + * + * @param {Array} queryStrings An array of key/value pairs + */ + dictionaryToQueryString: function (queryStrings) { + if (angular.isArray(queryStrings)) { + return _.map(queryStrings, function (item) { + var key = null; + var val = null; + for (var k in item) { + key = k; + val = item[k]; + break; + } + if (key === null || val === null) { + throw 'The object in the array was not formatted as a key/value pair'; + } + return encodeURIComponent(key) + '=' + encodeURIComponent(val); + }).join('&'); + } else if (angular.isObject(queryStrings)) { + //this allows for a normal object to be passed in (ie. a dictionary) + return decodeURIComponent($.param(queryStrings)); + } + throw 'The queryString parameter is not an array or object of key value pairs'; + }, + /** + * @ngdoc method + * @name umbraco.services.umbRequestHelper#getApiUrl + * @methodOf umbraco.services.umbRequestHelper + * @function + * + * @description + * This will return the webapi Url for the requested key based on the servervariables collection + * + * @param {string} apiName The webapi name that is found in the servervariables["umbracoUrls"] dictionary + * @param {string} actionName The webapi action name + * @param {object} queryStrings Can be either a string or an array containing key/value pairs + */ + getApiUrl: function (apiName, actionName, queryStrings) { + if (!Umbraco || !Umbraco.Sys || !Umbraco.Sys.ServerVariables || !Umbraco.Sys.ServerVariables['umbracoUrls']) { + throw 'No server variables defined!'; + } + if (!Umbraco.Sys.ServerVariables['umbracoUrls'][apiName]) { + throw 'No url found for api name ' + apiName; + } + return Umbraco.Sys.ServerVariables['umbracoUrls'][apiName] + actionName + (!queryStrings ? '' : '?' + (angular.isString(queryStrings) ? queryStrings : this.dictionaryToQueryString(queryStrings))); + }, + /** * @ngdoc function - * @name umbraco.services.umbModelMapper#convertToEntityBasic - * @methodOf umbraco.services.umbModelMapper + * @name umbraco.services.umbRequestHelper#resourcePromise + * @methodOf umbraco.services.umbRequestHelper * @function * * @description - * Converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model. - * @param {Object} source The source model - * @param {Number} source.id The node id of the model - * @param {String} source.name The node name - * @param {String} source.icon The models icon as a css class (.icon-doc) - * @param {Number} source.parentId The parentID, if no parent, set to -1 - * @param {path} source.path comma-separated string of ancestor IDs (-1,1234,1782,1234) - */ - - /** This converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model */ - convertToEntityBasic: function (source) { - var required = ["id", "name", "icon", "parentId", "path"]; - _.each(required, function (k) { - if (!_.has(source, k)) { - throw "The source object does not contain the property " + k; - } - }); - var optional = ["metaData", "key", "alias"]; - //now get the basic object - var result = _.pick(source, required.concat(optional)); - return result; - } - - }; -} -angular.module('umbraco.services').factory('umbModelMapper', umbModelMapper); - -/** + * This returns a promise with an underlying http call, it is a helper method to reduce + * the amount of duplicate code needed to query http resources and automatically handle any + * Http errors. See /docs/source/using-promises-resources.md + * + * @param {object} opts A mixed object which can either be a string representing the error message to be + * returned OR an object containing either: + * { success: successCallback, errorMsg: errorMessage } + * OR + * { success: successCallback, error: errorCallback } + * In both of the above, the successCallback must accept these parameters: data, status, headers, config + * If using the errorCallback it must accept these parameters: data, status, headers, config + * The success callback must return the data which will be resolved by the deferred object. + * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } + */ + resourcePromise: function (httpPromise, opts) { + var deferred = $q.defer(); + /** The default success callback used if one is not supplied in the opts */ + function defaultSuccess(data, status, headers, config) { + //when it's successful, just return the data + return data; + } + /** The default error callback used if one is not supplied in the opts */ + function defaultError(data, status, headers, config) { + return { + //NOTE: the default error message here should never be used based on the above docs! + errorMsg: angular.isString(opts) ? opts : 'An error occurred!', + data: data, + status: status + }; + } + //create the callbacs based on whats been passed in. + var callbacks = { + success: !opts || !opts.success ? defaultSuccess : opts.success, + error: !opts || !opts.error ? defaultError : opts.error + }; + httpPromise.success(function (data, status, headers, config) { + //invoke the callback + var result = callbacks.success.apply(this, [ + data, + status, + headers, + config + ]); + //when it's successful, just return the data + deferred.resolve(result); + }).error(function (data, status, headers, config) { + //invoke the callback + var result = callbacks.error.apply(this, [ + data, + status, + headers, + config + ]); + //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. + if (status >= 500 && status < 600) { + //show a ysod dialog + if (Umbraco.Sys.ServerVariables['isDebuggingEnabled'] === true) { + eventsService.emit('app.ysod', { + errorMsg: 'An error occured', + data: data + }); + } else { + //show a simple error notification + notificationsService.error('Server error', 'Contact administrator, see log for full details.
    ' + result.errorMsg + ''); + } + } + //return an error object including the error message for UI + deferred.reject({ + errorMsg: result.errorMsg, + data: result.data, + status: result.status + }); + }); + return deferred.promise; + }, + /** Used for saving media/content specifically */ + postSaveContent: function (args) { + if (!args.restApiUrl) { + throw 'args.restApiUrl is a required argument'; + } + if (!args.content) { + throw 'args.content is a required argument'; + } + if (!args.action) { + throw 'args.action is a required argument'; + } + if (!args.files) { + throw 'args.files is a required argument'; + } + if (!args.dataFormatter) { + throw 'args.dataFormatter is a required argument'; + } + var deferred = $q.defer(); + //save the active tab id so we can set it when the data is returned. + var activeTab = _.find(args.content.tabs, function (item) { + return item.active; + }); + var activeTabIndex = activeTab === undefined ? 0 : _.indexOf(args.content.tabs, activeTab); + //save the data + this.postMultiPartRequest(args.restApiUrl, { + key: 'contentItem', + value: args.dataFormatter(args.content, args.action) + }, function (data, formData) { + //now add all of the assigned files + for (var f in args.files) { + //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key + // so we know which property it belongs to on the server side + formData.append('file_' + args.files[f].alias, args.files[f].file); + } + }, function (data, status, headers, config) { + //success callback + //reset the tabs and set the active one + _.each(data.tabs, function (item) { + item.active = false; + }); + data.tabs[activeTabIndex].active = true; + //the data returned is the up-to-date data so the UI will refresh + deferred.resolve(data); + }, function (data, status, headers, config) { + //failure callback + //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. + if (status >= 500 && status < 600) { + //This is a bit of a hack to check if the error is due to a file being uploaded that is too large, + // we have to just check for the existence of a string value but currently that is the best way to + // do this since it's very hacky/difficult to catch this on the server + if (typeof data !== 'undefined' && typeof data.indexOf === 'function' && data.indexOf('Maximum request length exceeded') >= 0) { + notificationsService.error('Server error', 'The uploaded file was too large, check with your site administrator to adjust the maximum size allowed'); + } else if (Umbraco.Sys.ServerVariables['isDebuggingEnabled'] === true) { + //show a ysod dialog + eventsService.emit('app.ysod', { + errorMsg: 'An error occured', + data: data + }); + } else { + //show a simple error notification + notificationsService.error('Server error', 'Contact administrator, see log for full details.
    ' + data.ExceptionMessage + ''); + } + } + //return an error object including the error message for UI + deferred.reject({ + errorMsg: 'An error occurred', + data: data, + status: status + }); + }); + return deferred.promise; + }, + /** Posts a multi-part mime request to the server */ + postMultiPartRequest: function (url, jsonData, transformCallback, successCallback, failureCallback) { + //validate input, jsonData can be an array of key/value pairs or just one key/value pair. + if (!jsonData) { + throw 'jsonData cannot be null'; + } + if (angular.isArray(jsonData)) { + _.each(jsonData, function (item) { + if (!item.key || !item.value) { + throw 'jsonData array item must have both a key and a value property'; + } + }); + } else if (!jsonData.key || !jsonData.value) { + throw 'jsonData object must have both a key and a value property'; + } + $http({ + method: 'POST', + url: url, + //IMPORTANT!!! You might think this should be set to 'multipart/form-data' but this is not true because when we are sending up files + // the request needs to include a 'boundary' parameter which identifies the boundary name between parts in this multi-part request + // and setting the Content-type manually will not set this boundary parameter. For whatever reason, setting the Content-type to 'false' + // will force the request to automatically populate the headers properly including the boundary parameter. + headers: { 'Content-Type': false }, + transformRequest: function (data) { + var formData = new FormData(); + //add the json data + if (angular.isArray(data)) { + _.each(data, function (item) { + formData.append(item.key, !angular.isString(item.value) ? angular.toJson(item.value) : item.value); + }); + } else { + formData.append(data.key, !angular.isString(data.value) ? angular.toJson(data.value) : data.value); + } + //call the callback + if (transformCallback) { + transformCallback.apply(this, [ + data, + formData + ]); + } + return formData; + }, + data: jsonData + }).success(function (data, status, headers, config) { + if (successCallback) { + successCallback.apply(this, [ + data, + status, + headers, + config + ]); + } + }).error(function (data, status, headers, config) { + if (failureCallback) { + failureCallback.apply(this, [ + data, + status, + headers, + config + ]); + } + }); + } + }; + } + angular.module('umbraco.services').factory('umbRequestHelper', umbRequestHelper); + angular.module('umbraco.services').factory('userService', function ($rootScope, eventsService, $q, $location, $log, securityRetryQueue, authResource, dialogService, $timeout, angularHelper, $http) { + var currentUser = null; + var lastUserId = null; + var loginDialog = null; + //this tracks the last date/time that the user's remainingAuthSeconds was updated from the server + // this is used so that we know when to go and get the user's remaining seconds directly. + var lastServerTimeoutSet = null; + function openLoginDialog(isTimedOut) { + if (!loginDialog) { + loginDialog = dialogService.open({ + //very special flag which means that global events cannot close this dialog + manualClose: true, + template: 'views/common/dialogs/login.html', + modalClass: 'login-overlay', + animation: 'slide', + show: true, + callback: onLoginDialogClose, + dialogData: { isTimedOut: isTimedOut } + }); + } + } + function onLoginDialogClose(success) { + loginDialog = null; + if (success) { + securityRetryQueue.retryAll(currentUser.name); + } else { + securityRetryQueue.cancelAll(); + $location.path('/'); + } + } + /** + This methods will set the current user when it is resolved and + will then start the counter to count in-memory how many seconds they have + remaining on the auth session + */ + function setCurrentUser(usr) { + if (!usr.remainingAuthSeconds) { + throw 'The user object is invalid, the remainingAuthSeconds is required.'; + } + currentUser = usr; + lastServerTimeoutSet = new Date(); + //start the timer + countdownUserTimeout(); + } + /** + Method to count down the current user's timeout seconds, + this will continually count down their current remaining seconds every 5 seconds until + there are no more seconds remaining. + */ + function countdownUserTimeout() { + $timeout(function () { + if (currentUser) { + //countdown by 5 seconds since that is how long our timer is for. + currentUser.remainingAuthSeconds -= 5; + //if there are more than 30 remaining seconds, recurse! + if (currentUser.remainingAuthSeconds > 30) { + //we need to check when the last time the timeout was set from the server, if + // it has been more than 30 seconds then we'll manually go and retrieve it from the + // server - this helps to keep our local countdown in check with the true timeout. + if (lastServerTimeoutSet != null) { + var now = new Date(); + var seconds = (now.getTime() - lastServerTimeoutSet.getTime()) / 1000; + if (seconds > 30) { + //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we + // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait. + lastServerTimeoutSet = null; + //now go get it from the server + //NOTE: the safeApply because our timeout is set to not run digests (performance reasons) + angularHelper.safeApply($rootScope, function () { + authResource.getRemainingTimeoutSeconds().then(function (result) { + setUserTimeoutInternal(result); + }); + }); + } + } + //recurse the countdown! + countdownUserTimeout(); + } else { + //we are either timed out or very close to timing out so we need to show the login dialog. + if (Umbraco.Sys.ServerVariables.umbracoSettings.keepUserLoggedIn !== true) { + //NOTE: the safeApply because our timeout is set to not run digests (performance reasons) + angularHelper.safeApply($rootScope, function () { + try { + //NOTE: We are calling this again so that the server can create a log that the timeout has expired, we + // don't actually care about this result. + authResource.getRemainingTimeoutSeconds(); + } finally { + userAuthExpired(); + } + }); + } else { + //we've got less than 30 seconds remaining so let's check the server + if (lastServerTimeoutSet != null) { + //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we + // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait. + lastServerTimeoutSet = null; + //now go get it from the server + //NOTE: the safeApply because our timeout is set to not run digests (performance reasons) + angularHelper.safeApply($rootScope, function () { + authResource.getRemainingTimeoutSeconds().then(function (result) { + setUserTimeoutInternal(result); + }); + }); + } + //recurse the countdown! + countdownUserTimeout(); + } + } + } + }, 5000, //every 5 seconds + false); //false = do NOT execute a digest for every iteration + } + /** Called to update the current user's timeout */ + function setUserTimeoutInternal(newTimeout) { + var asNumber = parseFloat(newTimeout); + if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) { + currentUser.remainingAuthSeconds = newTimeout; + lastServerTimeoutSet = new Date(); + } + } + /** resets all user data, broadcasts the notAuthenticated event and shows the login dialog */ + function userAuthExpired(isLogout) { + //store the last user id and clear the user + if (currentUser && currentUser.id !== undefined) { + lastUserId = currentUser.id; + } + if (currentUser) { + currentUser.remainingAuthSeconds = 0; + } + lastServerTimeoutSet = null; + currentUser = null; + //broadcast a global event that the user is no longer logged in + eventsService.emit('app.notAuthenticated'); + openLoginDialog(isLogout === undefined ? true : !isLogout); + } + // Register a handler for when an item is added to the retry queue + securityRetryQueue.onItemAddedCallbacks.push(function (retryItem) { + if (securityRetryQueue.hasMore()) { + userAuthExpired(); + } + }); + return { + /** Internal method to display the login dialog */ + _showLoginDialog: function () { + openLoginDialog(); + }, + /** Returns a promise, sends a request to the server to check if the current cookie is authorized */ + isAuthenticated: function () { + //if we've got a current user then just return true + if (currentUser) { + var deferred = $q.defer(); + deferred.resolve(true); + return deferred.promise; + } + return authResource.isAuthenticated(); + }, + /** Returns a promise, sends a request to the server to validate the credentials */ + authenticate: function (login, password) { + return authResource.performLogin(login, password).then(this.setAuthenticationSuccessful); + }, + setAuthenticationSuccessful: function (data) { + //when it's successful, return the user data + setCurrentUser(data); + var result = { + user: data, + authenticated: true, + lastUserId: lastUserId, + loginType: 'credentials' + }; + //broadcast a global event + eventsService.emit('app.authenticated', result); + return result; + }, + /** Logs the user out + */ + logout: function () { + return authResource.performLogout().then(function (data) { + userAuthExpired(); + //done! + return null; + }); + }, + /** Refreshes the current user data with the data stored for the user on the server and returns it */ + refreshCurrentUser: function () { + var deferred = $q.defer(); + authResource.getCurrentUser().then(function (data) { + var result = { + user: data, + authenticated: true, + lastUserId: lastUserId, + loginType: 'implicit' + }; + setCurrentUser(data); + deferred.resolve(currentUser); + }, function () { + //it failed, so they are not logged in + deferred.reject(); + }); + return deferred.promise; + }, + /** Returns the current user object in a promise */ + getCurrentUser: function (args) { + var deferred = $q.defer(); + if (!currentUser) { + authResource.getCurrentUser().then(function (data) { + var result = { + user: data, + authenticated: true, + lastUserId: lastUserId, + loginType: 'implicit' + }; + if (args && args.broadcastEvent) { + //broadcast a global event, will inform listening controllers to load in the user specific data + eventsService.emit('app.authenticated', result); + } + setCurrentUser(data); + deferred.resolve(currentUser); + }, function () { + //it failed, so they are not logged in + deferred.reject(); + }); + } else { + deferred.resolve(currentUser); + } + return deferred.promise; + }, + /** Called whenever a server request is made that contains a x-umb-user-seconds response header for which we can update the user's remaining timeout seconds */ + setUserTimeout: function (newTimeout) { + setUserTimeoutInternal(newTimeout); + } + }; + }); + (function () { + 'use strict'; + function usersHelperService(localizationService) { + var userStates = [ + { + 'name': 'All', + 'key': 'All' + }, + { + 'value': 0, + 'name': 'Active', + 'key': 'Active', + 'color': 'success' + }, + { + 'value': 1, + 'name': 'Disabled', + 'key': 'Disabled', + 'color': 'danger' + }, + { + 'value': 2, + 'name': 'Locked out', + 'key': 'LockedOut', + 'color': 'danger' + }, + { + 'value': 3, + 'name': 'Invited', + 'key': 'Invited', + 'color': 'warning' + } + ]; + angular.forEach(userStates, function (userState) { + var key = 'user_state' + userState.key; + localizationService.localize(key).then(function (value) { + var reg = /^\[[\S\s]*]$/g; + var result = reg.test(value); + if (result === false) { + // Only translate if key exists + userState.name = value; + } + }); + }); + function getUserStateFromValue(value) { + var foundUserState; + angular.forEach(userStates, function (userState) { + if (userState.value === value) { + foundUserState = userState; + } + }); + return foundUserState; + } + function getUserStateByKey(key) { + var foundUserState; + angular.forEach(userStates, function (userState) { + if (userState.key === key) { + foundUserState = userState; + } + }); + return foundUserState; + } + function getUserStatesFilter(userStatesObject) { + var userStatesFilter = []; + for (var key in userStatesObject) { + if (userStatesObject.hasOwnProperty(key)) { + var userState = getUserStateByKey(key); + if (userState) { + userState.count = userStatesObject[key]; + userStatesFilter.push(userState); + } + } + } + return userStatesFilter; + } + //////////// + var service = { + getUserStateFromValue: getUserStateFromValue, + getUserStateByKey: getUserStateByKey, + getUserStatesFilter: getUserStatesFilter + }; + return service; + } + angular.module('umbraco.services').factory('usersHelper', usersHelperService); + }()); + /*Contains multiple services for various helper tasks */ + function versionHelper() { + return { + //see: https://gist.github.com/TheDistantSea/8021359 + versionCompare: function (v1, v2, options) { + var lexicographical = options && options.lexicographical, zeroExtend = options && options.zeroExtend, v1parts = v1.split('.'), v2parts = v2.split('.'); + function isValidPart(x) { + return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x); + } + if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { + return NaN; + } + if (zeroExtend) { + while (v1parts.length < v2parts.length) { + v1parts.push('0'); + } + while (v2parts.length < v1parts.length) { + v2parts.push('0'); + } + } + if (!lexicographical) { + v1parts = v1parts.map(Number); + v2parts = v2parts.map(Number); + } + for (var i = 0; i < v1parts.length; ++i) { + if (v2parts.length === i) { + return 1; + } + if (v1parts[i] === v2parts[i]) { + continue; + } else if (v1parts[i] > v2parts[i]) { + return 1; + } else { + return -1; + } + } + if (v1parts.length !== v2parts.length) { + return -1; + } + return 0; + } + }; + } + angular.module('umbraco.services').factory('versionHelper', versionHelper); + function dateHelper() { + return { + convertToServerStringTime: function (momentLocal, serverOffsetMinutes, format) { + //get the formatted offset time in HH:mm (server time offset is in minutes) + var formattedOffset = (serverOffsetMinutes > 0 ? '+' : '-') + moment().startOf('day').minutes(Math.abs(serverOffsetMinutes)).format('HH:mm'); + var server = moment.utc(momentLocal).utcOffset(formattedOffset); + return server.format(format ? format : 'YYYY-MM-DD HH:mm:ss'); + }, + convertToLocalMomentTime: function (strVal, serverOffsetMinutes) { + //get the formatted offset time in HH:mm (server time offset is in minutes) + var formattedOffset = (serverOffsetMinutes > 0 ? '+' : '-') + moment().startOf('day').minutes(Math.abs(serverOffsetMinutes)).format('HH:mm'); + //convert to the iso string format + var isoFormat = moment(strVal).format('YYYY-MM-DDTHH:mm:ss') + formattedOffset; + //create a moment with the iso format which will include the offset with the correct time + // then convert it to local time + return moment.parseZone(isoFormat).local(); + } + }; + } + angular.module('umbraco.services').factory('dateHelper', dateHelper); + function packageHelper(assetsService, treeService, eventsService, $templateCache) { + return { + /** Called when a package is installed, this resets a bunch of data and ensures the new package assets are loaded in */ + packageInstalled: function () { + //clears the tree + treeService.clearCache(); + //clears the template cache + $templateCache.removeAll(); + //emit event to notify anything else + eventsService.emit('app.reInitialize'); + } + }; + } + angular.module('umbraco.services').factory('packageHelper', packageHelper); + //TODO: I believe this is obsolete + function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) { + return { + /** sets the image's url, thumbnail and if its a folder */ + setImageData: function (img) { + img.isFolder = !mediaHelper.hasFilePropertyType(img); + if (!img.isFolder) { + img.thumbnail = mediaHelper.resolveFile(img, true); + img.image = mediaHelper.resolveFile(img, false); + } + }, + /** sets the images original size properties - will check if it is a folder and if so will just make it square */ + setOriginalSize: function (img, maxHeight) { + //set to a square by default + img.originalWidth = maxHeight; + img.originalHeight = maxHeight; + var widthProp = _.find(img.properties, function (v) { + return v.alias === 'umbracoWidth'; + }); + if (widthProp && widthProp.value) { + img.originalWidth = parseInt(widthProp.value, 10); + if (isNaN(img.originalWidth)) { + img.originalWidth = maxHeight; + } + } + var heightProp = _.find(img.properties, function (v) { + return v.alias === 'umbracoHeight'; + }); + if (heightProp && heightProp.value) { + img.originalHeight = parseInt(heightProp.value, 10); + if (isNaN(img.originalHeight)) { + img.originalHeight = maxHeight; + } + } + }, + /** sets the image style which get's used in the angular markup */ + setImageStyle: function (img, width, height, rightMargin, bottomMargin) { + img.style = { + width: width + 'px', + height: height + 'px', + 'margin-right': rightMargin + 'px', + 'margin-bottom': bottomMargin + 'px' + }; + img.thumbStyle = { + 'background-image': 'url(\'' + img.thumbnail + '\')', + 'background-repeat': 'no-repeat', + 'background-position': 'center', + 'background-size': Math.min(width, img.originalWidth) + 'px ' + Math.min(height, img.originalHeight) + 'px' + }; + }, + /** gets the image's scaled wdith based on the max row height */ + getScaledWidth: function (img, maxHeight) { + var scaled = img.originalWidth * maxHeight / img.originalHeight; + return scaled; //round down, we don't want it too big even by half a pixel otherwise it'll drop to the next row + //return Math.floor(scaled); + }, + /** returns the target row width taking into account how many images will be in the row and removing what the margin is */ + getTargetWidth: function (imgsPerRow, maxRowWidth, margin) { + //take into account the margin, we will have 1 less margin item than we have total images + return maxRowWidth - (imgsPerRow - 1) * margin; + }, + /** + This will determine the row/image height for the next collection of images which takes into account the + ideal image count per row. It will check if a row can be filled with this ideal count and if not - if there + are additional images available to fill the row it will keep calculating until they fit. + + It will return the calculated height and the number of images for the row. + + targetHeight = optional; + */ + getRowHeightForImages: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) { + var idealImages = imgs.slice(0, idealImgPerRow); + //get the target row width without margin + var targetRowWidth = this.getTargetWidth(idealImages.length, maxRowWidth, margin); + //this gets the image with the smallest height which equals the maximum we can scale up for this image block + var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight); + //if the max scale height is smaller than the min display height, we'll use the min display height + targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); + var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); + if (attemptedRowHeight != null) { + //if this is smaller than the min display then we need to use the min display, + // which means we'll need to remove one from the row so we can scale up to fill the row + if (attemptedRowHeight < minDisplayHeight) { + if (idealImages.length > 1) { + //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight + targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); + return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight); + } else { + //this will occur when we only have one image remaining in the row but it's still going to be too wide even when + // using the minimum display height specified. In this case we're going to have to just crop the image in it's center + // using the minimum display height and the full row width + return { + height: minDisplayHeight, + imgCount: 1 + }; + } + } else { + //success! + return { + height: attemptedRowHeight, + imgCount: idealImages.length + }; + } + } + //we know the width will fit in a row, but we now need to figure out if we can fill + // the entire row in the case that we have more images remaining than the idealImgPerRow. + if (idealImages.length === imgs.length) { + //we have no more remaining images to fill the space, so we'll just use the calc height + return { + height: targetHeight, + imgCount: idealImages.length + }; + } else if (idealImages.length === 1) { + //this will occur when we only have one image remaining in the row to process but it's not really going to fit ideally + // in the row. + return { + height: minDisplayHeight, + imgCount: 1 + }; + } else if (idealImages.length === idealImgPerRow && targetHeight < maxRowHeight) { + //if we're already dealing with the ideal images per row and it's not quite wide enough, we can scale up a little bit so + // long as the targetHeight is currently less than the maxRowHeight. The scale up will be half-way between our current + // target height and the maxRowHeight (we won't loop forever though - if there's a difference of 5 px we'll just quit) + while (targetHeight < maxRowHeight && maxRowHeight - targetHeight > 5) { + targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); + attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); + if (attemptedRowHeight != null) { + //success! + return { + height: attemptedRowHeight, + imgCount: idealImages.length + }; + } + } + //Ok, we couldn't actually scale it up with the ideal row count we'll just recurse with a lesser image count. + return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin); + } else if (targetHeight === maxRowHeight) { + //This is going to happen when: + // * We can fit a list of images in a row, but they come up too short (based on minDisplayHeight) + // * Then we'll try to remove an image, but when we try to scale to fit, the width comes up too narrow but the images are already at their + // maximum height (maxRowHeight) + // * So we're stuck, we cannot precicely fit the current list of images, so we'll render a row that will be max height but won't be wide enough + // which is better than rendering a row that is shorter than the minimum since that could be quite small. + return { + height: targetHeight, + imgCount: idealImages.length + }; + } else { + //we have additional images so we'll recurse and add 1 to the idealImgPerRow until it fits + return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow + 1, margin); + } + }, + performGetRowHeight: function (idealImages, targetRowWidth, minDisplayHeight, targetHeight) { + var currRowWidth = 0; + for (var i = 0; i < idealImages.length; i++) { + var scaledW = this.getScaledWidth(idealImages[i], targetHeight); + currRowWidth += scaledW; + } + if (currRowWidth > targetRowWidth) { + //get the new scaled height to fit + var newHeight = targetRowWidth * targetHeight / currRowWidth; + return newHeight; + } else if (idealImages.length === 1 && currRowWidth <= targetRowWidth && !idealImages[0].isFolder) { + //if there is only one image, then return the target height + return targetHeight; + } else if (currRowWidth / targetRowWidth > 0.9) { + //it's close enough, it's at least 90% of the width so we'll accept it with the target height + return targetHeight; + } else { + //if it's not successful, return null + return null; + } + }, + /** builds an image grid row */ + buildRow: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) { + var currRowWidth = 0; + var row = { images: [] }; + var imageRowHeight = this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); + var targetWidth = this.getTargetWidth(imageRowHeight.imgCount, maxRowWidth, margin); + var sizes = []; + //loop through the images we know fit into the height + for (var i = 0; i < imageRowHeight.imgCount; i++) { + //get the lower width to ensure it always fits + var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height)); + if (currRowWidth + scaledWidth <= targetWidth) { + currRowWidth += scaledWidth; + sizes.push({ + width: scaledWidth, + //ensure that the height is rounded + height: Math.round(imageRowHeight.height) + }); + row.images.push(imgs[i]); + } else if (imageRowHeight.imgCount === 1 && row.images.length === 0) { + //the image is simply too wide, we'll crop/center it + sizes.push({ + width: maxRowWidth, + //ensure that the height is rounded + height: Math.round(imageRowHeight.height) + }); + row.images.push(imgs[i]); + } else { + //the max width has been reached + break; + } + } + //loop through the images for the row and apply the styles + for (var j = 0; j < row.images.length; j++) { + var bottomMargin = margin; + //make the margin 0 for the last one + if (j === row.images.length - 1) { + margin = 0; + } + this.setImageStyle(row.images[j], sizes[j].width, sizes[j].height, margin, bottomMargin); + } + if (row.images.length === 1 && totalRemaining > 1) { + //if there's only one image on the row and there are more images remaining, set the container to max width + row.images[0].style.width = maxRowWidth + 'px'; + } + return row; + }, + /** Returns the maximum image scaling height for the current image collection */ + getMaxScaleableHeight: function (imgs, maxRowHeight) { + var smallestHeight = _.min(imgs, function (item) { + return item.originalHeight; + }).originalHeight; + //adjust the smallestHeight if it is larger than the static max row height + if (smallestHeight > maxRowHeight) { + smallestHeight = maxRowHeight; + } + return smallestHeight; + }, + /** Creates the image grid with calculated widths/heights for images to fill the grid nicely */ + buildGrid: function (images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin, imagesOnly) { + var rows = []; + var imagesProcessed = 0; + //first fill in all of the original image sizes and URLs + for (var i = startingIndex; i < images.length; i++) { + var item = images[i]; + this.setImageData(item); + this.setOriginalSize(item, maxRowHeight); + if (imagesOnly && !item.isFolder && !item.thumbnail) { + images.splice(i, 1); + i--; + } + } + while (imagesProcessed + startingIndex < images.length) { + //get the maxHeight for the current un-processed images + var currImgs = images.slice(imagesProcessed); + //build the row + var remaining = images.length - imagesProcessed; + var row = this.buildRow(currImgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, remaining); + if (row.images.length > 0) { + rows.push(row); + imagesProcessed += row.images.length; + } else { + if (currImgs.length > 0) { + throw 'Could not fill grid with all images, images remaining: ' + currImgs.length; + } + //if there was nothing processed, exit + break; + } + } + return rows; + } + }; + } + angular.module('umbraco.services').factory('umbPhotoFolderHelper', umbPhotoFolderHelper); + /** + * @ngdoc function + * @name umbraco.services.umbModelMapper + * @function + * + * @description + * Utility class to map/convert models + */ + function umbModelMapper() { + return { + /** + * @ngdoc function + * @name umbraco.services.umbModelMapper#convertToEntityBasic + * @methodOf umbraco.services.umbModelMapper + * @function + * + * @description + * Converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model. + * @param {Object} source The source model + * @param {Number} source.id The node id of the model + * @param {String} source.name The node name + * @param {String} source.icon The models icon as a css class (.icon-doc) + * @param {Number} source.parentId The parentID, if no parent, set to -1 + * @param {path} source.path comma-separated string of ancestor IDs (-1,1234,1782,1234) + */ + /** This converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model */ + convertToEntityBasic: function (source) { + var required = [ + 'id', + 'name', + 'icon', + 'parentId', + 'path' + ]; + _.each(required, function (k) { + if (!_.has(source, k)) { + throw 'The source object does not contain the property ' + k; + } + }); + var optional = [ + 'metaData', + 'key', + 'alias' + ]; + //now get the basic object + var result = _.pick(source, required.concat(optional)); + return result; + } + }; + } + angular.module('umbraco.services').factory('umbModelMapper', umbModelMapper); + /** * @ngdoc function * @name umbraco.services.umbSessionStorage * @function @@ -9855,759 +9276,499 @@ angular.module('umbraco.services').factory('umbModelMapper', umbModelMapper); * @description * Used to get/set things in browser sessionStorage but always prefixes keys with "umb_" and converts json vals so there is no overlap * with any sessionStorage created by a developer. - */ -function umbSessionStorage($window) { - - //gets the sessionStorage object if available, otherwise just uses a normal object - // - required for unit tests. - var storage = $window['sessionStorage'] ? $window['sessionStorage'] : {}; - - return { - - get: function (key) { - return angular.fromJson(storage["umb_" + key]); - }, - - set : function(key, value) { - storage["umb_" + key] = angular.toJson(value); - } - - }; -} -angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorage); - -/** + */ + function umbSessionStorage($window) { + //gets the sessionStorage object if available, otherwise just uses a normal object + // - required for unit tests. + var storage = $window['sessionStorage'] ? $window['sessionStorage'] : {}; + return { + get: function (key) { + return angular.fromJson(storage['umb_' + key]); + }, + set: function (key, value) { + storage['umb_' + key] = angular.toJson(value); + } + }; + } + angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorage); + /** * @ngdoc function * @name umbraco.services.updateChecker * @function * * @description * used to check for updates and display a notifcation - */ -function updateChecker($http, umbRequestHelper) { - return { - - /** - * @ngdoc function - * @name umbraco.services.updateChecker#check - * @methodOf umbraco.services.updateChecker - * @function - * - * @description - * Called to load in the legacy tree js which is required on startup if a user is logged in or - * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. - */ - check: function() { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "updateCheckApiBaseUrl", - "GetCheck")), - 'Failed to retrieve update status'); - } - }; -} -angular.module('umbraco.services').factory('updateChecker', updateChecker); - -/** + */ + function updateChecker($http, umbRequestHelper) { + return { + /** + * @ngdoc function + * @name umbraco.services.updateChecker#check + * @methodOf umbraco.services.updateChecker + * @function + * + * @description + * Called to load in the legacy tree js which is required on startup if a user is logged in or + * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. + */ + check: function () { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('updateCheckApiBaseUrl', 'GetCheck')), 'Failed to retrieve update status'); + } + }; + } + angular.module('umbraco.services').factory('updateChecker', updateChecker); + /** * @ngdoc service * @name umbraco.services.umbPropertyEditorHelper * @description A helper object used for property editors -**/ -function umbPropEditorHelper() { - return { - /** - * @ngdoc function - * @name getImagePropertyValue - * @methodOf umbraco.services.umbPropertyEditorHelper - * @function - * - * @description - * Returns the correct view path for a property editor, it will detect if it is a full virtual path but if not then default to the internal umbraco one - * - * @param {string} input the view path currently stored for the property editor - */ - getViewPath: function(input, isPreValue) { - var path = String(input); - - if (path.startsWith('/')) { - - //This is an absolute path, so just leave it - return path; - } else { - - if (path.indexOf("/") >= 0) { - //This is a relative path, so just leave it - return path; - } else { - if (!isPreValue) { - //i.e. views/propertyeditors/fileupload/fileupload.html - return "views/propertyeditors/" + path + "/" + path + ".html"; - } else { - //i.e. views/prevalueeditors/requiredfield.html - return "views/prevalueeditors/" + path + ".html"; - } - } - - } - } - }; -} -angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorHelper); - - -/** -* @ngdoc service -* @name umbraco.services.umbDataFormatter -* @description A helper object used to format/transform JSON Umbraco data, mostly used for persisting data to the server -**/ -function umbDataFormatter() { - return { - - formatContentTypePostData: function (displayModel, action) { - - //create the save model from the display model - var saveModel = _.pick(displayModel, - 'compositeContentTypes', 'isContainer', 'allowAsRoot', 'allowedTemplates', 'allowedContentTypes', - 'alias', 'description', 'thumbnail', 'name', 'id', 'icon', 'trashed', - 'key', 'parentId', 'alias', 'path'); - - //TODO: Map these - saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { return t.alias; }); - saveModel.defaultTemplate = displayModel.defaultTemplate ? displayModel.defaultTemplate.alias : null; - var realGroups = _.reject(displayModel.groups, function(g) { - //do not include these tabs - return g.tabState === "init"; - }); - saveModel.groups = _.map(realGroups, function (g) { - - var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name'); - - var realProperties = _.reject(g.properties, function (p) { - //do not include these properties - return p.propertyState === "init" || p.inherited === true; - }); - - var saveProperties = _.map(realProperties, function (p) { - var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile'); - return saveProperty; - }); - - saveGroup.properties = saveProperties; - - //if this is an inherited group and there are not non-inherited properties on it, then don't send up the data - if (saveGroup.inherited === true && saveProperties.length === 0) { - return null; - } - - return saveGroup; - }); - - //we don't want any null groups - saveModel.groups = _.reject(saveModel.groups, function(g) { - return !g; - }); - - return saveModel; - }, - - /** formats the display model used to display the data type to the model used to save the data type */ - formatDataTypePostData: function(displayModel, preValues, action) { - var saveModel = { - parentId: displayModel.parentId, - id: displayModel.id, - name: displayModel.name, - selectedEditor: displayModel.selectedEditor, - //set the action on the save model - action: action, - preValues: [] - }; - for (var i = 0; i < preValues.length; i++) { - - saveModel.preValues.push({ - key: preValues[i].alias, - value: preValues[i].value - }); - } - return saveModel; - }, - - /** formats the display model used to display the member to the model used to save the member */ - formatMemberPostData: function(displayModel, action) { - //this is basically the same as for media but we need to explicitly add the username,email, password to the save model - - var saveModel = this.formatMediaPostData(displayModel, action); - - saveModel.key = displayModel.key; - - var genericTab = _.find(displayModel.tabs, function (item) { - return item.id === 0; - }); - - //map the member login, email, password and groups - var propLogin = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_login"; - }); - var propEmail = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_email"; - }); - var propPass = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_password"; - }); - var propGroups = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_membergroup"; - }); - saveModel.email = propEmail.value; - saveModel.username = propLogin.value; - saveModel.password = propPass.value; - - var selectedGroups = []; - for (var n in propGroups.value) { - if (propGroups.value[n] === true) { - selectedGroups.push(n); - } - } - saveModel.memberGroups = selectedGroups; - - //turn the dictionary into an array of pairs - var memberProviderPropAliases = _.pairs(displayModel.fieldConfig); - _.each(displayModel.tabs, function (tab) { - _.each(tab.properties, function (prop) { - var foundAlias = _.find(memberProviderPropAliases, function(item) { - return prop.alias === item[1]; - }); - if (foundAlias) { - //we know the current property matches an alias, now we need to determine which membership provider property it was for - // by looking at the key - switch (foundAlias[0]) { - case "umbracoMemberLockedOut": - saveModel.isLockedOut = prop.value.toString() === "1" ? true : false; - break; - case "umbracoMemberApproved": - saveModel.isApproved = prop.value.toString() === "1" ? true : false; - break; - case "umbracoMemberComments": - saveModel.comments = prop.value; - break; - } - } - }); - }); - - - - return saveModel; - }, - - /** formats the display model used to display the media to the model used to save the media */ - formatMediaPostData: function(displayModel, action) { - //NOTE: the display model inherits from the save model so we can in theory just post up the display model but - // we don't want to post all of the data as it is unecessary. - var saveModel = { - id: displayModel.id, - properties: [], - name: displayModel.name, - contentTypeAlias: displayModel.contentTypeAlias, - parentId: displayModel.parentId, - //set the action on the save model - action: action - }; - - _.each(displayModel.tabs, function (tab) { - - _.each(tab.properties, function (prop) { - - //don't include the custom generic tab properties - if (!prop.alias.startsWith("_umb_")) { - saveModel.properties.push({ - id: prop.id, - alias: prop.alias, - value: prop.value - }); - } - - }); - }); - - return saveModel; - }, - - /** formats the display model used to display the content to the model used to save the content */ - formatContentPostData: function (displayModel, action) { - - //this is basically the same as for media but we need to explicitly add some extra properties - var saveModel = this.formatMediaPostData(displayModel, action); - - var genericTab = _.find(displayModel.tabs, function (item) { - return item.id === 0; - }); - - var propExpireDate = _.find(genericTab.properties, function(item) { - return item.alias === "_umb_expiredate"; - }); - var propReleaseDate = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_releasedate"; - }); - var propTemplate = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_template"; - }); - saveModel.expireDate = propExpireDate.value; - saveModel.releaseDate = propReleaseDate.value; - saveModel.templateAlias = propTemplate.value; - - return saveModel; - } - }; -} -angular.module('umbraco.services').factory('umbDataFormatter', umbDataFormatter); - - - -/** - * @ngdoc service - * @name umbraco.services.windowResizeListener - * @function - * - * @description - * A single window resize listener... we don't want to have more than one in theory to ensure that - * there aren't too many events raised. This will debounce the event with 100 ms intervals and force - * a $rootScope.$apply when changed and notify all listeners - * - */ -function windowResizeListener($rootScope) { - - var WinReszier = (function () { - var registered = []; - var inited = false; - var resize = _.debounce(function(ev) { - notify(); - }, 100); - var notify = function () { - var h = $(window).height(); - var w = $(window).width(); - //execute all registrations inside of a digest - $rootScope.$apply(function() { - for (var i = 0, cnt = registered.length; i < cnt; i++) { - registered[i].apply($(window), [{ width: w, height: h }]); +**/ + function umbPropEditorHelper() { + return { + /** + * @ngdoc function + * @name getImagePropertyValue + * @methodOf umbraco.services.umbPropertyEditorHelper + * @function + * + * @description + * Returns the correct view path for a property editor, it will detect if it is a full virtual path but if not then default to the internal umbraco one + * + * @param {string} input the view path currently stored for the property editor + */ + getViewPath: function (input, isPreValue) { + var path = String(input); + if (path.startsWith('/')) { + //This is an absolute path, so just leave it + return path; + } else { + if (path.indexOf('/') >= 0) { + //This is a relative path, so just leave it + return path; + } else { + if (!isPreValue) { + //i.e. views/propertyeditors/fileupload/fileupload.html + return 'views/propertyeditors/' + path + '/' + path + '.html'; + } else { + //i.e. views/prevalueeditors/requiredfield.html + return 'views/prevalueeditors/' + path + '.html'; + } + } } - }); + } + }; + } + angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorHelper); + /** +* @ngdoc service +* @name umbraco.services.queryStrings +* @description A helper used to get query strings in the real URL (not the hash URL) +**/ + function queryStrings($window) { + var pl = /\+/g; + // Regex for replacing addition symbol with a space + var search = /([^&=]+)=?([^&]*)/g; + var decode = function (s) { + return decodeURIComponent(s.replace(pl, ' ')); }; return { - register: function (fn) { - registered.push(fn); - if (inited === false) { - $(window).bind('resize', resize); - inited = true; - } - }, - unregister: function (fn) { - var index = registered.indexOf(fn); - if (index > -1) { - registered.splice(index, 1); + getParams: function () { + var match; + var query = $window.location.search.substring(1); + var urlParams = {}; + while (match = search.exec(query)) { + urlParams[decode(match[1])] = decode(match[2]); } + return urlParams; } }; - }()); - - return { - - /** - * Register a callback for resizing - * @param {Function} cb + } + angular.module('umbraco.services').factory('queryStrings', queryStrings); + /** + * @ngdoc service + * @name umbraco.services.windowResizeListener + * @function + * + * @description + * A single window resize listener... we don't want to have more than one in theory to ensure that + * there aren't too many events raised. This will debounce the event with 100 ms intervals and force + * a $rootScope.$apply when changed and notify all listeners + * + */ + function windowResizeListener($rootScope) { + var WinReszier = function () { + var registered = []; + var inited = false; + var resize = _.debounce(function (ev) { + notify(); + }, 100); + var notify = function () { + var h = $(window).height(); + var w = $(window).width(); + //execute all registrations inside of a digest + $rootScope.$apply(function () { + for (var i = 0, cnt = registered.length; i < cnt; i++) { + registered[i].apply($(window), [{ + width: w, + height: h + }]); + } + }); + }; + return { + register: function (fn) { + registered.push(fn); + if (inited === false) { + $(window).bind('resize', resize); + inited = true; + } + }, + unregister: function (fn) { + var index = registered.indexOf(fn); + if (index > -1) { + registered.splice(index, 1); + } + } + }; + }(); + return { + /** + * Register a callback for resizing + * @param {Function} cb */ - register: function (cb) { - WinReszier.register(cb); - }, - - /** - * Removes a registered callback - * @param {Function} cb + register: function (cb) { + WinReszier.register(cb); + }, + /** + * Removes a registered callback + * @param {Function} cb */ - unregister: function(cb) { - WinReszier.unregister(cb); - } - - }; -} -angular.module('umbraco.services').factory('windowResizeListener', windowResizeListener); -/** - * @ngdoc service - * @name umbraco.services.xmlhelper - * @function - * - * @description - * Used to convert legacy xml data to json and back again + unregister: function (cb) { + WinReszier.unregister(cb); + } + }; + } + angular.module('umbraco.services').factory('windowResizeListener', windowResizeListener); + /** + * @ngdoc service + * @name umbraco.services.xmlhelper + * @function + * + * @description + * Used to convert legacy xml data to json and back again */ -function xmlhelper($http) { - /* - Copyright 2011 Abdulla Abdurakhmanov - Original sources are available at https://code.google.com/p/x2js/ - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + function xmlhelper($http) { + /* + Copyright 2011 Abdulla Abdurakhmanov + Original sources are available at https://code.google.com/p/x2js/ + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ - - function X2JS() { - var VERSION = "1.0.11"; - var escapeMode = false; - - var DOMNodeTypes = { - ELEMENT_NODE: 1, - TEXT_NODE: 3, - CDATA_SECTION_NODE: 4, - DOCUMENT_NODE: 9 - }; - - function getNodeLocalName(node) { - var nodeLocalName = node.localName; - if (nodeLocalName == null) { - nodeLocalName = node.baseName; - } // Yeah, this is IE!! - - if (nodeLocalName === null || nodeLocalName === "") { - nodeLocalName = node.nodeName; - } // =="" is IE too - - return nodeLocalName; - } - - function getNodePrefix(node) { - return node.prefix; - } - - function escapeXmlChars(str) { - if (typeof (str) === "string") { - return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); - } else { - return str; + function X2JS() { + var VERSION = '1.0.11'; + var escapeMode = false; + var DOMNodeTypes = { + ELEMENT_NODE: 1, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + DOCUMENT_NODE: 9 + }; + function getNodeLocalName(node) { + var nodeLocalName = node.localName; + if (nodeLocalName == null) { + nodeLocalName = node.baseName; + } + // Yeah, this is IE!! + if (nodeLocalName === null || nodeLocalName === '') { + nodeLocalName = node.nodeName; + } + // =="" is IE too + return nodeLocalName; } - } - - function unescapeXmlChars(str) { - return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(///g, '\/'); - } - - function parseDOMChildren(node) { - var result, child, childName; - - if (node.nodeType === DOMNodeTypes.DOCUMENT_NODE) { - result = {}; - child = node.firstChild; - childName = getNodeLocalName(child); - result[childName] = parseDOMChildren(child); - return result; + function getNodePrefix(node) { + return node.prefix; + } + function escapeXmlChars(str) { + if (typeof str === 'string') { + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); + } else { + return str; + } } - else { - - if (node.nodeType === DOMNodeTypes.ELEMENT_NODE) { + function unescapeXmlChars(str) { + return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '\'').replace(///g, '/'); + } + function parseDOMChildren(node) { + var result, child, childName; + if (node.nodeType === DOMNodeTypes.DOCUMENT_NODE) { result = {}; - result.__cnt = 0; - var nodeChildren = node.childNodes; - - // Children nodes - for (var cidx = 0; cidx < nodeChildren.length; cidx++) { - child = nodeChildren.item(cidx); // nodeChildren[cidx]; - childName = getNodeLocalName(child); - - result.__cnt++; - if (result[childName] === null) { - result[childName] = parseDOMChildren(child); - result[childName + "_asArray"] = new Array(1); - result[childName + "_asArray"][0] = result[childName]; - } - else { - if (result[childName] !== null) { - if (!(result[childName] instanceof Array)) { - var tmpObj = result[childName]; - result[childName] = []; - result[childName][0] = tmpObj; - - result[childName + "_asArray"] = result[childName]; + child = node.firstChild; + childName = getNodeLocalName(child); + result[childName] = parseDOMChildren(child); + return result; + } else { + if (node.nodeType === DOMNodeTypes.ELEMENT_NODE) { + result = {}; + result.__cnt = 0; + var nodeChildren = node.childNodes; + // Children nodes + for (var cidx = 0; cidx < nodeChildren.length; cidx++) { + child = nodeChildren.item(cidx); + // nodeChildren[cidx]; + childName = getNodeLocalName(child); + result.__cnt++; + if (result[childName] === null) { + result[childName] = parseDOMChildren(child); + result[childName + '_asArray'] = new Array(1); + result[childName + '_asArray'][0] = result[childName]; + } else { + if (result[childName] !== null) { + if (!(result[childName] instanceof Array)) { + var tmpObj = result[childName]; + result[childName] = []; + result[childName][0] = tmpObj; + result[childName + '_asArray'] = result[childName]; + } } + var aridx = 0; + while (result[childName][aridx] !== null) { + aridx++; + } + result[childName][aridx] = parseDOMChildren(child); } - var aridx = 0; - while (result[childName][aridx] !== null) { - aridx++; + } + // Attributes + for (var aidx = 0; aidx < node.attributes.length; aidx++) { + var attr = node.attributes.item(aidx); + // [aidx]; + result.__cnt++; + result['_' + attr.name] = attr.value; + } + // Node namespace prefix + var nodePrefix = getNodePrefix(node); + if (nodePrefix !== null && nodePrefix !== '') { + result.__cnt++; + result.__prefix = nodePrefix; + } + if (result.__cnt === 1 && result['#text'] !== null) { + result = result['#text']; + } + if (result['#text'] !== null) { + result.__text = result['#text']; + if (escapeMode) { + result.__text = unescapeXmlChars(result.__text); } - - (result[childName])[aridx] = parseDOMChildren(child); + delete result['#text']; + delete result['#text_asArray']; } - } - - // Attributes - for (var aidx = 0; aidx < node.attributes.length; aidx++) { - var attr = node.attributes.item(aidx); // [aidx]; - result.__cnt++; - result["_" + attr.name] = attr.value; - } - - // Node namespace prefix - var nodePrefix = getNodePrefix(node); - if (nodePrefix !== null && nodePrefix !== "") { - result.__cnt++; - result.__prefix = nodePrefix; - } - - if (result.__cnt === 1 && result["#text"] !== null) { - result = result["#text"]; - } - - if (result["#text"] !== null) { - result.__text = result["#text"]; - if (escapeMode) { - result.__text = unescapeXmlChars(result.__text); + if (result['#cdata-section'] != null) { + result.__cdata = result['#cdata-section']; + delete result['#cdata-section']; + delete result['#cdata-section_asArray']; + } + if (result.__text != null || result.__cdata != null) { + result.toString = function () { + return (this.__text != null ? this.__text : '') + (this.__cdata != null ? this.__cdata : ''); + }; + } + return result; + } else { + if (node.nodeType === DOMNodeTypes.TEXT_NODE || node.nodeType === DOMNodeTypes.CDATA_SECTION_NODE) { + return node.nodeValue; } - - delete result["#text"]; - delete result["#text_asArray"]; - } - if (result["#cdata-section"] != null) { - result.__cdata = result["#cdata-section"]; - delete result["#cdata-section"]; - delete result["#cdata-section_asArray"]; - } - - if (result.__text != null || result.__cdata != null) { - result.toString = function () { - return (this.__text != null ? this.__text : '') + (this.__cdata != null ? this.__cdata : ''); - }; } - return result; } - else { - if (node.nodeType === DOMNodeTypes.TEXT_NODE || node.nodeType === DOMNodeTypes.CDATA_SECTION_NODE) { - return node.nodeValue; + } + function startTag(jsonObj, element, attrList, closed) { + var resultStr = '<' + (jsonObj != null && jsonObj.__prefix != null ? jsonObj.__prefix + ':' : '') + element; + if (attrList != null) { + for (var aidx = 0; aidx < attrList.length; aidx++) { + var attrName = attrList[aidx]; + var attrVal = jsonObj[attrName]; + resultStr += ' ' + attrName.substr(1) + '=\'' + attrVal + '\''; } } + if (!closed) { + resultStr += '>'; + } else { + resultStr += '/>'; + } + return resultStr; } - } - - function startTag(jsonObj, element, attrList, closed) { - var resultStr = "<" + ((jsonObj != null && jsonObj.__prefix != null) ? (jsonObj.__prefix + ":") : "") + element; - if (attrList != null) { - for (var aidx = 0; aidx < attrList.length; aidx++) { - var attrName = attrList[aidx]; - var attrVal = jsonObj[attrName]; - resultStr += " " + attrName.substr(1) + "='" + attrVal + "'"; - } - } - if (!closed) { - resultStr += ">"; - } else { - resultStr += "/>"; + function endTag(jsonObj, elementName) { + return ''; } - - return resultStr; - } - - function endTag(jsonObj, elementName) { - return ""; - } - - function endsWith(str, suffix) { - return str.indexOf(suffix, str.length - suffix.length) !== -1; - } - - function jsonXmlSpecialElem(jsonObj, jsonObjField) { - if (endsWith(jsonObjField.toString(), ("_asArray")) || jsonObjField.toString().indexOf("_") === 0 || (jsonObj[jsonObjField] instanceof Function)) { - return true; - } else { - return false; + function endsWith(str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; } - } - - function jsonXmlElemCount(jsonObj) { - var elementsCnt = 0; - if (jsonObj instanceof Object) { - for (var it in jsonObj) { - if (jsonXmlSpecialElem(jsonObj, it)) { - continue; - } - elementsCnt++; + function jsonXmlSpecialElem(jsonObj, jsonObjField) { + if (endsWith(jsonObjField.toString(), '_asArray') || jsonObjField.toString().indexOf('_') === 0 || jsonObj[jsonObjField] instanceof Function) { + return true; + } else { + return false; } } - return elementsCnt; - } - - function parseJSONAttributes(jsonObj) { - var attrList = []; - if (jsonObj instanceof Object) { - for (var ait in jsonObj) { - if (ait.toString().indexOf("__") === -1 && ait.toString().indexOf("_") === 0) { - attrList.push(ait); + function jsonXmlElemCount(jsonObj) { + var elementsCnt = 0; + if (jsonObj instanceof Object) { + for (var it in jsonObj) { + if (jsonXmlSpecialElem(jsonObj, it)) { + continue; + } + elementsCnt++; } } + return elementsCnt; } - - return attrList; - } - - function parseJSONTextAttrs(jsonTxtObj) { - var result = ""; - - if (jsonTxtObj.__cdata != null) { - result += ""; - } - - if (jsonTxtObj.__text != null) { - if (escapeMode) { - result += escapeXmlChars(jsonTxtObj.__text); - } else { - result += jsonTxtObj.__text; + function parseJSONAttributes(jsonObj) { + var attrList = []; + if (jsonObj instanceof Object) { + for (var ait in jsonObj) { + if (ait.toString().indexOf('__') === -1 && ait.toString().indexOf('_') === 0) { + attrList.push(ait); + } + } } + return attrList; } - return result; - } - - function parseJSONTextObject(jsonTxtObj) { - var result = ""; - - if (jsonTxtObj instanceof Object) { - result += parseJSONTextAttrs(jsonTxtObj); - } - else { - if (jsonTxtObj != null) { + function parseJSONTextAttrs(jsonTxtObj) { + var result = ''; + if (jsonTxtObj.__cdata != null) { + result += ''; + } + if (jsonTxtObj.__text != null) { if (escapeMode) { - result += escapeXmlChars(jsonTxtObj); + result += escapeXmlChars(jsonTxtObj.__text); } else { - result += jsonTxtObj; + result += jsonTxtObj.__text; } } + return result; } - - - return result; - } - - function parseJSONArray(jsonArrRoot, jsonArrObj, attrList) { - var result = ""; - if (jsonArrRoot.length === 0) { - result += startTag(jsonArrRoot, jsonArrObj, attrList, true); - } - else { - for (var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) { - result += startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false); - result += parseJSONObject(jsonArrRoot[arIdx]); - result += endTag(jsonArrRoot[arIdx], jsonArrObj); + function parseJSONTextObject(jsonTxtObj) { + var result = ''; + if (jsonTxtObj instanceof Object) { + result += parseJSONTextAttrs(jsonTxtObj); + } else { + if (jsonTxtObj != null) { + if (escapeMode) { + result += escapeXmlChars(jsonTxtObj); + } else { + result += jsonTxtObj; + } + } } + return result; } - return result; - } - - function parseJSONObject(jsonObj) { - var result = ""; - - var elementsCnt = jsonXmlElemCount(jsonObj); - - if (elementsCnt > 0) { - for (var it in jsonObj) { - if (jsonXmlSpecialElem(jsonObj, it)) { - continue; + function parseJSONArray(jsonArrRoot, jsonArrObj, attrList) { + var result = ''; + if (jsonArrRoot.length === 0) { + result += startTag(jsonArrRoot, jsonArrObj, attrList, true); + } else { + for (var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) { + result += startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false); + result += parseJSONObject(jsonArrRoot[arIdx]); + result += endTag(jsonArrRoot[arIdx], jsonArrObj); } - - var subObj = jsonObj[it]; - var attrList = parseJSONAttributes(subObj); - - if (subObj === null || subObj === undefined) { - result += startTag(subObj, it, attrList, true); - } else { - if (subObj instanceof Object) { - - if (subObj instanceof Array) { - result += parseJSONArray(subObj, it, attrList); - } else { - var subObjElementsCnt = jsonXmlElemCount(subObj); - if (subObjElementsCnt > 0 || subObj.__text !== null || subObj.__cdata !== null) { - result += startTag(subObj, it, attrList, false); - result += parseJSONObject(subObj); - result += endTag(subObj, it); + } + return result; + } + function parseJSONObject(jsonObj) { + var result = ''; + var elementsCnt = jsonXmlElemCount(jsonObj); + if (elementsCnt > 0) { + for (var it in jsonObj) { + if (jsonXmlSpecialElem(jsonObj, it)) { + continue; + } + var subObj = jsonObj[it]; + var attrList = parseJSONAttributes(subObj); + if (subObj === null || subObj === undefined) { + result += startTag(subObj, it, attrList, true); + } else { + if (subObj instanceof Object) { + if (subObj instanceof Array) { + result += parseJSONArray(subObj, it, attrList); } else { - result += startTag(subObj, it, attrList, true); + var subObjElementsCnt = jsonXmlElemCount(subObj); + if (subObjElementsCnt > 0 || subObj.__text !== null || subObj.__cdata !== null) { + result += startTag(subObj, it, attrList, false); + result += parseJSONObject(subObj); + result += endTag(subObj, it); + } else { + result += startTag(subObj, it, attrList, true); + } } + } else { + result += startTag(subObj, it, attrList, false); + result += parseJSONTextObject(subObj); + result += endTag(subObj, it); } - - } else { - result += startTag(subObj, it, attrList, false); - result += parseJSONTextObject(subObj); - result += endTag(subObj, it); } } } + result += parseJSONTextObject(jsonObj); + return result; } - result += parseJSONTextObject(jsonObj); - - return result; + this.parseXmlString = function (xmlDocStr) { + var xmlDoc; + if (window.DOMParser) { + var parser = new window.DOMParser(); + xmlDoc = parser.parseFromString(xmlDocStr, 'text/xml'); + } else { + // IE :( + if (xmlDocStr.indexOf('') + 2); + } + xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xmlDocStr); + } + return xmlDoc; + }; + this.xml2json = function (xmlDoc) { + return parseDOMChildren(xmlDoc); + }; + this.xml_str2json = function (xmlDocStr) { + var xmlDoc = this.parseXmlString(xmlDocStr); + return this.xml2json(xmlDoc); + }; + this.json2xml_str = function (jsonObj) { + return parseJSONObject(jsonObj); + }; + this.json2xml = function (jsonObj) { + var xmlDocStr = this.json2xml_str(jsonObj); + return this.parseXmlString(xmlDocStr); + }; + this.getVersion = function () { + return VERSION; + }; + this.escapeMode = function (enabled) { + escapeMode = enabled; + }; } - - this.parseXmlString = function (xmlDocStr) { - var xmlDoc; - if (window.DOMParser) { - var parser = new window.DOMParser(); - xmlDoc = parser.parseFromString(xmlDocStr, "text/xml"); - } - else { - // IE :( - if (xmlDocStr.indexOf("") + 2); - } - xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(xmlDocStr); - } - return xmlDoc; - }; - - this.xml2json = function (xmlDoc) { - return parseDOMChildren(xmlDoc); - }; - - this.xml_str2json = function (xmlDocStr) { - var xmlDoc = this.parseXmlString(xmlDocStr); - return this.xml2json(xmlDoc); - }; - - this.json2xml_str = function (jsonObj) { - return parseJSONObject(jsonObj); - }; - - this.json2xml = function (jsonObj) { - var xmlDocStr = this.json2xml_str(jsonObj); - return this.parseXmlString(xmlDocStr); - }; - - this.getVersion = function () { - return VERSION; - }; - - this.escapeMode = function (enabled) { - escapeMode = enabled; + var x2js = new X2JS(); + return { + /** Called to load in the legacy tree js which is required on startup if a user is logged in or + after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */ + toJson: function (xml) { + var json = x2js.xml_str2json(xml); + return json; + }, + fromJson: function (json) { + var xml = x2js.json2xml_str(json); + return xml; + } }; } - - var x2js = new X2JS(); - return { - /** Called to load in the legacy tree js which is required on startup if a user is logged in or - after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */ - toJson: function (xml) { - var json = x2js.xml_str2json(xml); - return json; - }, - fromJson: function (json) { - var xml = x2js.json2xml_str(json); - return xml; - } - }; -} -angular.module('umbraco.services').factory('xmlhelper', xmlhelper); - -})(); \ No newline at end of file + angular.module('umbraco.services').factory('xmlhelper', xmlhelper); +}()); \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Js/web.config b/src/Umbraco.SampleSite.Website/Umbraco/Js/web.config new file mode 100644 index 00000000..6903c396 --- /dev/null +++ b/src/Umbraco.SampleSite.Website/Umbraco/Js/web.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Breadcrumb.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Breadcrumb.cshtml index 8ba3dce3..7be87c07 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Breadcrumb.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Breadcrumb.cshtml @@ -1,24 +1,25 @@ -@inherits Umbraco.Web.Macros.PartialViewMacroPage +@using Umbraco.Web +@inherits Umbraco.Web.Macros.PartialViewMacroPage @* - This snippet makes a breadcrumb of parents using an unordered html list. + This snippet makes a breadcrumb of parents using an unordered HTML list. How it works: - It uses the Ancestors() method to get all parents and then generates links so the visitor can go back - Finally it outputs the name of the current page (without a link) *@ -@{ var selection = CurrentPage.Ancestors(); } +@{ var selection = Model.Content.Ancestors().ToArray(); } -@if (selection.Any()) +@if (selection.Length > 0) { } \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Gallery.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Gallery.cshtml index 52d87c5b..a34eab0e 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Gallery.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Gallery.cshtml @@ -1,50 +1,47 @@ @inherits Umbraco.Web.Macros.PartialViewMacroPage @* - Macro to display a gallery of images from media the media section. + Macro to display a gallery of images from the Media section. Works with either a 'Single Media Picker' or a 'Multiple Media Picker' macro parameter (see below). How it works: - Confirm the macro parameter has been passed in with a value - - Loop through all the media Id's passed in (might be a single item, might be many) + - Loop through all the media Ids passed in (might be a single item, might be many) - Display any individual images, as well as any folders of images Macro Parameters To Create, for this macro to work: Alias:mediaIds Name:Select folders and/or images Type: Multiple Media Picker - Type: (note: you can use a Single Media Picker if that's more appropriate to your needs) + Type: (note: You can use a Single Media Picker if that's more appropriate to your needs) *@ -@{ var mediaIds = Model.MacroParameters["mediaIds"]; } +@{ var mediaIds = Model.MacroParameters["mediaIds"] as string; } + @if (mediaIds != null) { -
      - @foreach (var mediaId in mediaIds.ToString().Split(',')) +
      + @foreach (var mediaId in mediaIds.Split(',')) { - var media = Umbraco.Media(mediaId); + var media = Umbraco.TypedMedia(mediaId); @* a single image *@ if (media.DocumentTypeAlias == "Image") { - @Render(media); + @Render(media as Image); } @* a folder with images under it *@ - if (media.Children("Image").Any()) + foreach (var image in media.Children()) { - foreach (var image in media.Children("Image")) - { - @Render(image); - } + @Render(image); } - } -
    +
    } -@helper Render(dynamic item) +@helper Render(Image item) { -
  • - - @item.Name +
  • +
    } \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListAncestorsFromCurrentPage.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListAncestorsFromCurrentPage.cshtml index 9d1284be..f022aa5d 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListAncestorsFromCurrentPage.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListAncestorsFromCurrentPage.cshtml @@ -1,24 +1,25 @@ +@using Umbraco.Web @inherits Umbraco.Web.Macros.PartialViewMacroPage @* - This snippet makes a list of links to the of parents of the current page using an unordered html list. + This snippet makes a list of links to the of parents of the current page using an unordered HTML list. How it works: - It uses the Ancestors() method to get all parents and then generates links so the visitor can go back - Finally it outputs the name of the current page (without a link) *@ -@{ var selection = CurrentPage.Ancestors(); } +@{ var selection = Model.Content.Ancestors().ToArray(); } -@if (selection.Any()) +@if (selection.Length > 0) {
      @* For each page in the ancestors collection which have been ordered by Level (so we start with the highest top node first) *@ - @foreach (var item in selection.OrderBy("Level")) + @foreach (var item in selection.OrderBy(x => x.Level)) {
    • @item.Name »
    • } @* Display the current page as the last item in the list *@ -
    • @CurrentPage.Name
    • +
    • @Model.Content.Name
    } diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesFromChangeableSource.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesFromChangeableSource.cshtml index ea45c13a..6fbe125d 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesFromChangeableSource.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesFromChangeableSource.cshtml @@ -1,3 +1,4 @@ +@using Umbraco.Web @inherits Umbraco.Web.Macros.PartialViewMacroPage @* @@ -13,20 +14,19 @@ *@ @{ var startNodeId = Model.MacroParameters["startNodeId"]; } + @if (startNodeId != null) { @* Get the starting page *@ - var startNode = Umbraco.Content(startNodeId); - var selection = startNode.Children.Where("Visible"); + var startNode = Umbraco.TypedContent(startNodeId); + var selection = startNode.Children.Where(x => x.IsVisible()).ToArray(); - if (selection.Any()) + if (selection.Length > 0) { } diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesFromCurrentPage.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesFromCurrentPage.cshtml index 78a3b0d1..e6606d62 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesFromCurrentPage.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesFromCurrentPage.cshtml @@ -1,8 +1,17 @@ +@using Umbraco.Web @inherits Umbraco.Web.Macros.PartialViewMacroPage -@{ var selection = CurrentPage.Children.Where("Visible"); } +@* + This snippet makes a list of links to the of children of the current page using an unordered HTML list. -@if (selection.Any()) + How it works: + - It uses the Children method to get all child pages + - It then generates links so the visitor can go to each page +*@ + +@{ var selection = Model.Content.Children.Where(x => x.IsVisible()).ToArray(); } + +@if (selection.Length > 0) {
      @foreach (var item in selection) diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByDate.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByDate.cshtml index 0a449419..2c2cc442 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByDate.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByDate.cshtml @@ -1,11 +1,23 @@ +@using Umbraco.Web @inherits Umbraco.Web.Macros.PartialViewMacroPage -@{ var selection = CurrentPage.Children.Where("Visible").OrderBy("CreateDate desc"); } -@* OrderBy() takes the property to sort by and optionally order desc/asc *@ +@* + This snippet makes a list of links to the of children of the current page using an unordered HTML list. -
        - @foreach (var item in selection) - { -
      • @item.Name
      • - } -
      + How it works: + - It uses the Children method to get all child pages + - It then uses the OrderByDescending() method, which takes the property to sort. In this case the page's creation date. + - It then generates links so the visitor can go to each page +*@ + +@{ var selection = Model.Content.Children.Where(x => x.IsVisible()).OrderByDescending(x => x.CreateDate).ToArray(); } + +@if (selection.Length > 0) +{ +
        + @foreach (var item in selection) + { +
      • @item.Name
      • + } +
      +} diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByName.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByName.cshtml index 8f333163..dd5e00cc 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByName.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByName.cshtml @@ -1,11 +1,23 @@ +@using Umbraco.Web @inherits Umbraco.Web.Mvc.UmbracoTemplatePage -@{ var selection = CurrentPage.Children.Where("Visible").OrderBy("Name"); } -@* OrderBy() takes the property to sort by *@ +@* + This snippet makes a list of links to the of children of the current page using an unordered HTML list. -
        - @foreach (var item in selection) - { -
      • @item.Name
      • - } -
      + How it works: + - It uses the Children method to get all child pages + - It then uses the OrderBy() method, which takes the property to sort. In this case, the page's name. + - It then generates links so the visitor can go to each page +*@ + +@{ var selection = Model.Content.Children.Where(x => x.IsVisible()).OrderBy(x => x.Name).ToArray(); } + +@if (selection.Length > 0) +{ +
        + @foreach (var item in selection) + { +
      • @item.Name
      • + } +
      +} \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByProperty.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByProperty.cshtml index b989e94b..db0f91c4 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByProperty.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByProperty.cshtml @@ -1,3 +1,4 @@ +@using Umbraco.Web @inherits Umbraco.Web.Macros.PartialViewMacroPage @* @@ -15,12 +16,15 @@ @{ var propertyAlias = Model.MacroParameters["propertyAlias"]; } @if (propertyAlias != null) { - var selection = CurrentPage.Children.Where("Visible").OrderBy(propertyAlias); + var selection = Model.Content.Children.Where(x => x.IsVisible()).OrderBy(x => x.GetPropertyValue(propertyAlias.ToString())).ToArray(); -
        - @foreach (var item in selection) - { -
      • @item.Name
      • - } -
      + if (selection.Length > 0) + { +
        + @foreach (var item in selection) + { +
      • @item.Name
      • + } +
      + } } diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesWithDoctype.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesWithDoctype.cshtml index eb9812e5..100d5022 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesWithDoctype.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListChildPagesWithDoctype.cshtml @@ -1,19 +1,17 @@ +@using Umbraco.Core.Models +@using Umbraco.Web @inherits Umbraco.Web.Macros.PartialViewMacroPage @* - This snippet shows how simple it is to fetch only children of a certain Document Type using Razor. - Be sure to change "DocumentTypeAlias" below to match your needs, such as "TextPage" or "NewsItems". + This snippet shows how simple it is to fetch only children of a certain Document Type. + + Be sure to change "IPublishedContent" below to match your needs, such as "TextPage" or "NewsItem". (You can find the alias of your Document Type by editing it in the Settings section) *@ -@{ var selection = CurrentPage.Children("DocumentTypeAlias").Where("Visible"); } -@* - As an example of more querying, if you have a true/false property with the alias of shouldBeFeatured: - var selection= CurrentPage.Children("DocumentTypeAlias").Where("shouldBeFeatured == true").Where("Visible"); -*@ - +@{ var selection = Model.Content.Children().Where(x => x.IsVisible()).ToArray(); } -@if (selection.Any()) +@if (selection.Length > 0) {
        @foreach (var item in selection) diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml index 68f4b380..1e752827 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml @@ -1,31 +1,36 @@ @inherits Umbraco.Web.Mvc.UmbracoTemplatePage +@using Umbraco.Core.Models +@using Umbraco.Web @* This snippet creates links for every single page (no matter how deep) below - the page currently being viewed by the website visitor, displayed as nested unordered html lists. + the page currently being viewed by the website visitor, displayed as nested unordered HTML lists. *@ -@{ var selection = CurrentPage.Children.Where("Visible"); } +@{ var selection = Model.Content.Children.Where(x => x.IsVisible()).ToArray(); } @* Ensure that the Current Page has children *@ -@if (selection.Any()) +@if (selection.Length > 0) { @* Get the first page in the children, where the property umbracoNaviHide is not True *@ - var naviLevel = CurrentPage.FirstChild().Where("Visible").Level; + var naviLevel = selection[0].Level; @* Add in level for a CSS hook *@ -
          - @* For each child page where the property umbracoNaviHide is not True *@ +
            + @* Loop through the selection *@ @foreach (var item in selection) {
          • @item.Name @* if this child page has any children, where the property umbracoNaviHide is not True *@ - @if (item.Children.Where("Visible").Any()) - { - @* Call our helper to display the children *@ - @childPages(item.Children) + @{ + var children = item.Children.Where(x => x.IsVisible()).ToArray(); + if (children.Length > 0) + { + @* Call our helper to display the children *@ + @ChildPages(children) + } }
          • } @@ -33,26 +38,29 @@ } -@helper childPages(dynamic selection) +@helper ChildPages(IPublishedContent[] selection) { @* Ensure that we have a collection of pages *@ - if (selection.Any()) + if (selection.Length > 0) { - @* Get the first page in pages and get the level *@ - var naviLevel = selection.First().Level; + @* Get the first page in pages and get the level *@ + var naviLevel = selection[0].Level; - @* Add in level for a CSS hook *@ + @* Add in level for a CSS hook *@
              - @foreach (var item in selection.Where("Visible")) + @foreach (var item in selection) {
            • @item.Name - @* if the this page has any children, where the property umbracoNaviHide is not True *@ - @if (item.Children.Where("Visible").Any()) - { - @* Call our helper to display the children *@ - @childPages(item.Children) + @* if the page has any children, where the property umbracoNaviHide is not True *@ + @{ + var children = item.Children.Where(x => x.IsVisible()).ToArray(); + if (children.Length > 0) + { + @* Recurse and call our helper to display the children *@ + @ChildPages(children) + } }
            • } diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListImagesFromMediaFolder.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListImagesFromMediaFolder.cshtml index 5d318aa1..3303418a 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListImagesFromMediaFolder.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/ListImagesFromMediaFolder.cshtml @@ -5,7 +5,7 @@ How it works: - Confirm the macro parameter has been passed in with a value - - Loop through all the media Id's passed in (might be a single item, might be many) + - Loop through all the media Ids passed in (might be a single item, might be many) - Display any individual images, as well as any folders of images Macro Parameters To Create, for this macro to work: @@ -15,17 +15,17 @@ @{ var mediaId = Model.MacroParameters["mediaId"]; } @if (mediaId != null) { - @* Get all the media item associated with the id passed in *@ - var media = Umbraco.Media(mediaId); - var selection = media.Children("Image"); + @* Get the media item associated with the id passed in *@ + var media = Umbraco.TypedMedia(mediaId); + var selection = media.Children().ToArray(); - if (selection.Any()) + if (selection.Length > 0) {
                @foreach (var item in selection) {
              • - @item.Name + @item.Name
              • }
              diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/MultinodeTree-picker.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/MultinodeTree-picker.cshtml index ca79ad56..a8df4b9a 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/MultinodeTree-picker.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/MultinodeTree-picker.cshtml @@ -1,21 +1,25 @@ +@using Umbraco.Core.Models +@using Umbraco.Web @inherits Umbraco.Web.Macros.PartialViewMacroPage @* - This snippet lists the items from a Multinode tree picker, using the pickers default settings. - Content Values stored as xml. + This snippet lists the items from a Multinode tree picker, using the picker's default settings. + Content Values stored as XML. To get it working with any site's data structure, set the selection equal to the property which has the multinode treepicker (so: replace "PropertyWithPicker" with the alias of your property). *@ -@{ var selection = CurrentPage.PropertyWithPicker.Split(','); } +@{ var selection = Model.Content.GetPropertyValue>("PropertyWithPicker").ToArray(); } -
                - @foreach (var id in selection) - { - var item = Umbraco.Content(id); -
              • - @item.Name -
              • - } -
              \ No newline at end of file +@if (selection.Length > 0) +{ +
                + @foreach (var item in selection) + { +
              • + @item.Name +
              • + } +
              +} \ No newline at end of file diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Navigation.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Navigation.cshtml index 19c0199d..9775c81a 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Navigation.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/Navigation.cshtml @@ -1,18 +1,22 @@ +@using Umbraco.Web @inherits Umbraco.Web.Macros.PartialViewMacroPage @* This snippet displays a list of links of the pages immediately under the top-most page in the content tree. This is the home page for a standard website. - It also highlights the current active page/section in the navigation with the css class "current". + It also highlights the current active page/section in the navigation with the CSS class "current". *@ -@{ var selection = CurrentPage.Site().Children.Where("Visible"); } +@{ var selection = Model.Content.Site().Children.Where(x => x.IsVisible()).ToArray(); } -
                - @foreach (var item in selection) - { -
              • - @item.Name -
              • - } -
              +@if (selection.Length > 0) +{ +
                + @foreach (var item in selection) + { +
              • + @item.Name +
              • + } +
              +} diff --git a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/SiteMap.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/SiteMap.cshtml index 0fd074cd..46ead8a2 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/SiteMap.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/PartialViewMacros/Templates/SiteMap.cshtml @@ -1,13 +1,15 @@ +@using Umbraco.Core.Models +@using Umbraco.Web @inherits Umbraco.Web.Macros.PartialViewMacroPage @* - This snippet makes a list of links of all visible pages of the site, as nested unordered html lists. + This snippet makes a list of links of all visible pages of the site, as nested unordered HTML lists. How it works: - It uses a custom Razor helper called Traverse() to select and display the markup and links. *@ -@{ var selection = CurrentPage.Site(); } +@{ var selection = Model.Content.Site(); }
              @* Render the sitemap by passing the root node to the traverse helper, below *@ @@ -15,17 +17,17 @@
              -@* Helper method to travers through all descendants *@ -@helper Traverse(dynamic node) +@* Helper method to traverse through all descendants *@ +@helper Traverse(IPublishedContent node) { @* Update the level to reflect how deep you want the sitemap to go *@ - var maxLevelForSitemap = 4; + const int maxLevelForSitemap = 4; @* Select visible children *@ - var selection = node.Children.Where("Visible").Where("Level <= " + maxLevelForSitemap); + var selection = node.Children.Where(x => x.IsVisible() && x.Level <= maxLevelForSitemap).ToArray(); @* If any items are returned, render a list *@ - if (selection.Any()) + if (selection.Length > 0) {
                @foreach (var item in selection) diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Views/Default.cshtml b/src/Umbraco.SampleSite.Website/Umbraco/Views/Default.cshtml index 6fd0a232..015bdfb3 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Views/Default.cshtml +++ b/src/Umbraco.SampleSite.Website/Umbraco/Views/Default.cshtml @@ -84,6 +84,7 @@ @if (isDebug) { + @Html.RenderProfiler() } diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/iconpicker.html b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/iconpicker.html index 96ab9904..4e482c26 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/iconpicker.html +++ b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/iconpicker.html @@ -7,7 +7,8 @@ style="width: 100%" ng-model="searchTerm" class="umb-search-field search-query input-block-level" - placeholder="Filter..." + localize="placeholder" + placeholder="@placeholders_filter" no-dirty-check> @@ -17,12 +18,12 @@
                diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/legacydelete.html b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/legacydelete.html index 5545dace..b6026e33 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/legacydelete.html +++ b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/legacydelete.html @@ -3,7 +3,7 @@

                - Are you sure you want to delete {{currentNode.name}} ? + Are you sure you want to delete {{currentNode.name}} ?

                diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/linkpicker.html b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/linkpicker.html index a1728f30..70d6cab4 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/linkpicker.html +++ b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/linkpicker.html @@ -1,8 +1,8 @@
                - \ No newline at end of file + diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/login.html b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/login.html index 446cf915..ee0235d7 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/login.html +++ b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/login.html @@ -1,148 +1,244 @@ -
                - -
                - - - -
                diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/user.html b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/user.html index 20dc41d8..0344f287 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/user.html +++ b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/user.html @@ -24,7 +24,7 @@

                {{user.name}}

                - @@ -42,7 +42,7 @@
                -
                External login providers
                +
                External login providers
                @@ -55,7 +55,7 @@
                External login providers
                onclick="document.forms.oauthloginform.submit();"> - Link your {{login.caption}} account + Link your {{login.caption}} account @@ -67,7 +67,7 @@
                External login providers
                name="provider" value="{{login.authType}}"> - Un-link your {{login.caption}} account + Un-link your {{login.caption}} account
                diff --git a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/ysod.html b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/ysod.html index 17348679..56f11c39 100644 --- a/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/ysod.html +++ b/src/Umbraco.SampleSite.Website/Umbraco/Views/common/dialogs/ysod.html @@ -1,4 +1,4 @@ -
                +