From a8e1de47e6525f64c55bd5b36907edae9e8ed451 Mon Sep 17 00:00:00 2001 From: Aaron Sadler Date: Mon, 19 Oct 2020 11:26:38 +0100 Subject: [PATCH 1/5] Installed uSync --- TestSite/App_Plugins/uSync8/addons.json | 92 ++++ .../uSync8/backoffice/uSync8/dashboard.html | 14 + .../uSync8/uSyncDashboardController.js | 58 +++ TestSite/App_Plugins/uSync8/changeDialog.html | 84 ++++ .../uSync8/changeDialogController.js | 57 +++ .../uSync8/components/uSyncProgressView.html | 42 ++ .../uSync8/components/uSyncReportView.html | 87 ++++ .../components/uSyncReportViewComponent.js | 113 +++++ .../components/usyncProgressViewComponent.js | 30 ++ TestSite/App_Plugins/uSync8/lang/en-US.xml | 55 +++ TestSite/App_Plugins/uSync8/lang/nl-NL.xml | 51 +++ TestSite/App_Plugins/uSync8/package.manifest | 22 + .../App_Plugins/uSync8/settings/default.html | 83 ++++ .../uSync8/settings/expansion.html | 18 + .../uSync8/settings/expansionController.js | 20 + .../App_Plugins/uSync8/settings/settings.html | 144 +++++++ .../uSync8/settings/settingsController.js | 50 +++ .../uSync8/settings/uSyncController.js | 370 ++++++++++++++++ TestSite/App_Plugins/uSync8/uSyncHub.js | 96 +++++ TestSite/App_Plugins/uSync8/uSyncService.js | 105 +++++ TestSite/App_Plugins/uSync8/uSync_871.js | 1 + TestSite/App_Plugins/uSync8/usync.css | 399 ++++++++++++++++++ TestSite/App_Plugins/uSync8/usync_871.css | 1 + TestSite/TestSite.csproj | 40 ++ TestSite/Web.config | 242 +++++------ TestSite/config/uSync8.config | 55 +++ TestSite/packages.config | 5 + .../v8/Content/about-this-starter-kit.config | 41 ++ TestSite/uSync/v8/Content/about-us.config | 117 +++++ TestSite/uSync/v8/Content/another-one.config | 90 ++++ TestSite/uSync/v8/Content/banjo.config | 50 +++ TestSite/uSync/v8/Content/biker-jacket.config | 72 ++++ TestSite/uSync/v8/Content/blog.config | 47 +++ TestSite/uSync/v8/Content/bowling-ball.config | 50 +++ TestSite/uSync/v8/Content/contact.config | 44 ++ TestSite/uSync/v8/Content/home.config | 109 +++++ .../uSync/v8/Content/jan-skovgaard.config | 47 +++ .../uSync/v8/Content/jeavon-leopold.config | 47 +++ .../uSync/v8/Content/jeroen-breuer.config | 47 +++ TestSite/uSync/v8/Content/jumpsuit.config | 50 +++ TestSite/uSync/v8/Content/knitted-west.config | 50 +++ TestSite/uSync/v8/Content/lee-kelleher.config | 47 +++ .../uSync/v8/Content/matt-brailsford.config | 47 +++ TestSite/uSync/v8/Content/my-blog-post.config | 73 ++++ TestSite/uSync/v8/Content/people.config | 44 ++ .../uSync/v8/Content/ping-pong-ball.config | 50 +++ TestSite/uSync/v8/Content/products.config | 47 +++ TestSite/uSync/v8/Content/tattoo.config | 50 +++ .../v8/Content/this-will-be-great.config | 88 ++++ .../todo-list-for-the-starter-kit.config | 68 +++ TestSite/uSync/v8/Content/unicorn.config | 50 +++ TestSite/uSync/v8/ContentTypes/blog.config | 62 +++ .../uSync/v8/ContentTypes/blogpost.config | 89 ++++ TestSite/uSync/v8/ContentTypes/contact.config | 123 ++++++ .../uSync/v8/ContentTypes/contentbase.config | 56 +++ .../uSync/v8/ContentTypes/contentpage.config | 26 ++ TestSite/uSync/v8/ContentTypes/feature.config | 56 +++ TestSite/uSync/v8/ContentTypes/home.config | 270 ++++++++++++ .../v8/ContentTypes/navigationbase.config | 71 ++++ TestSite/uSync/v8/ContentTypes/people.config | 47 +++ TestSite/uSync/v8/ContentTypes/person.config | 138 ++++++ TestSite/uSync/v8/ContentTypes/product.config | 155 +++++++ .../uSync/v8/ContentTypes/products.config | 62 +++ .../uSync/v8/DataTypes/ApprovedColor.config | 12 + ...BlogHowManyPostsShouldBeShownSlider.config | 16 + .../DataTypes/BlogpostCategoriesTags.config | 13 + .../uSync/v8/DataTypes/CheckboxList.config | 11 + .../ContactContactIntroRichTextEditor.config | 36 ++ ...ContactMapCoordinatesOpenStreetMaps.config | 9 + .../ContactPickAContactFormFormPicker.config | 11 + .../ContactPickAContactFormFormPicker1.config | 11 + .../ContentBaseContentGridLayout.config | 96 +++++ .../uSync/v8/DataTypes/ContentPicker.config | 13 + TestSite/uSync/v8/DataTypes/DatePicker.config | 12 + .../v8/DataTypes/DatePickerWithTime.config | 12 + TestSite/uSync/v8/DataTypes/Dropdown.config | 12 + .../v8/DataTypes/DropdownMultiple.config | 12 + .../HomeCallToActionLinkContentPicker.config | 13 + .../HomeColorThemeRadioButtonList.config | 24 ++ .../v8/DataTypes/HomeContentGridLayout.config | 117 +++++ .../DataTypes/HomeFontRadioButtonList.config | 24 ++ .../v8/DataTypes/HomeLogoMediaPicker.config | 15 + .../uSync/v8/DataTypes/ImageCropper.config | 11 + .../uSync/v8/DataTypes/LabelBigint.config | 11 + .../uSync/v8/DataTypes/LabelDatetime.config | 11 + .../uSync/v8/DataTypes/LabelDecimal.config | 11 + .../uSync/v8/DataTypes/LabelInteger.config | 11 + .../uSync/v8/DataTypes/LabelString.config | 11 + TestSite/uSync/v8/DataTypes/LabelTime.config | 11 + .../uSync/v8/DataTypes/ListViewContent.config | 53 +++ .../uSync/v8/DataTypes/ListViewMedia.config | 53 +++ .../uSync/v8/DataTypes/ListViewMembers.config | 59 +++ .../uSync/v8/DataTypes/MediaPicker.config | 15 + .../uSync/v8/DataTypes/MemberPicker.config | 9 + .../uSync/v8/DataTypes/MultiURLPicker.config | 14 + .../v8/DataTypes/MultipleMediaPicker.config | 15 + .../NavigationBaseKeywordsTags.config | 13 + TestSite/uSync/v8/DataTypes/Numeric.config | 9 + ...leFeaturedPeopleMultinodeTreepicker.config | 16 + .../v8/DataTypes/PersonDepartmentTags.config | 13 + .../DataTypes/PersonPhotoMediaPicker.config | 15 + .../v8/DataTypes/ProductCategoryTags.config | 13 + .../ProductFeaturesNestedContent.config | 22 + .../DataTypes/ProductPhotosMediaPicker.config | 15 + .../v8/DataTypes/ProductPriceDecimal.config | 9 + ...ProductsDefaultCurrencyDropdownList.config | 29 ++ ...FeaturedProductsMultinodeTreepicker.config | 16 + TestSite/uSync/v8/DataTypes/Radiobox.config | 11 + .../uSync/v8/DataTypes/RichtextEditor.config | 14 + TestSite/uSync/v8/DataTypes/Tags.config | 13 + TestSite/uSync/v8/DataTypes/Textarea.config | 12 + TestSite/uSync/v8/DataTypes/Textstring.config | 11 + TestSite/uSync/v8/DataTypes/TrueFalse.config | 12 + TestSite/uSync/v8/DataTypes/Upload.config | 9 + TestSite/uSync/v8/Languages/en-us.config | 6 + .../uSync/v8/Macros/featuredProduct.config | 19 + .../uSync/v8/Macros/latestBlogposts.config | 25 ++ .../uSync/v8/Macros/renderUmbracoForm.config | 31 ++ TestSite/uSync/v8/Media/banjo.config | 18 + TestSite/uSync/v8/Media/biker-jacket.config | 18 + TestSite/uSync/v8/Media/bowling-ball.config | 18 + TestSite/uSync/v8/Media/design.config | 14 + TestSite/uSync/v8/Media/jan-skovgaard.config | 18 + TestSite/uSync/v8/Media/jeavon-leopold.config | 18 + TestSite/uSync/v8/Media/jeroen-breuer.config | 18 + TestSite/uSync/v8/Media/jumpsuit.config | 18 + TestSite/uSync/v8/Media/knitted-west.config | 18 + TestSite/uSync/v8/Media/lee-kelleher.config | 18 + .../uSync/v8/Media/matt-brailsford.config | 18 + TestSite/uSync/v8/Media/people.config | 14 + TestSite/uSync/v8/Media/ping-pong-ball.config | 18 + TestSite/uSync/v8/Media/products.config | 14 + TestSite/uSync/v8/Media/tattoo.config | 18 + .../Media/umbraco-campari-meeting-room.config | 18 + TestSite/uSync/v8/Media/unicorn.config | 18 + TestSite/uSync/v8/MediaTypes/file.config | 65 +++ TestSite/uSync/v8/MediaTypes/folder.config | 21 + TestSite/uSync/v8/MediaTypes/image.config | 93 ++++ TestSite/uSync/v8/Templates/blog.config | 5 + TestSite/uSync/v8/Templates/blogpost.config | 5 + TestSite/uSync/v8/Templates/contact.config | 5 + .../uSync/v8/Templates/contentpage.config | 5 + TestSite/uSync/v8/Templates/home.config | 5 + TestSite/uSync/v8/Templates/master.config | 5 + TestSite/uSync/v8/Templates/people.config | 5 + TestSite/uSync/v8/Templates/person.config | 5 + TestSite/uSync/v8/Templates/product.config | 5 + TestSite/uSync/v8/Templates/products.config | 5 + TestSite/uSync/v8/usync.config | 4 + 149 files changed, 6417 insertions(+), 121 deletions(-) create mode 100644 TestSite/App_Plugins/uSync8/addons.json create mode 100644 TestSite/App_Plugins/uSync8/backoffice/uSync8/dashboard.html create mode 100644 TestSite/App_Plugins/uSync8/backoffice/uSync8/uSyncDashboardController.js create mode 100644 TestSite/App_Plugins/uSync8/changeDialog.html create mode 100644 TestSite/App_Plugins/uSync8/changeDialogController.js create mode 100644 TestSite/App_Plugins/uSync8/components/uSyncProgressView.html create mode 100644 TestSite/App_Plugins/uSync8/components/uSyncReportView.html create mode 100644 TestSite/App_Plugins/uSync8/components/uSyncReportViewComponent.js create mode 100644 TestSite/App_Plugins/uSync8/components/usyncProgressViewComponent.js create mode 100644 TestSite/App_Plugins/uSync8/lang/en-US.xml create mode 100644 TestSite/App_Plugins/uSync8/lang/nl-NL.xml create mode 100644 TestSite/App_Plugins/uSync8/package.manifest create mode 100644 TestSite/App_Plugins/uSync8/settings/default.html create mode 100644 TestSite/App_Plugins/uSync8/settings/expansion.html create mode 100644 TestSite/App_Plugins/uSync8/settings/expansionController.js create mode 100644 TestSite/App_Plugins/uSync8/settings/settings.html create mode 100644 TestSite/App_Plugins/uSync8/settings/settingsController.js create mode 100644 TestSite/App_Plugins/uSync8/settings/uSyncController.js create mode 100644 TestSite/App_Plugins/uSync8/uSyncHub.js create mode 100644 TestSite/App_Plugins/uSync8/uSyncService.js create mode 100644 TestSite/App_Plugins/uSync8/uSync_871.js create mode 100644 TestSite/App_Plugins/uSync8/usync.css create mode 100644 TestSite/App_Plugins/uSync8/usync_871.css create mode 100644 TestSite/config/uSync8.config create mode 100644 TestSite/uSync/v8/Content/about-this-starter-kit.config create mode 100644 TestSite/uSync/v8/Content/about-us.config create mode 100644 TestSite/uSync/v8/Content/another-one.config create mode 100644 TestSite/uSync/v8/Content/banjo.config create mode 100644 TestSite/uSync/v8/Content/biker-jacket.config create mode 100644 TestSite/uSync/v8/Content/blog.config create mode 100644 TestSite/uSync/v8/Content/bowling-ball.config create mode 100644 TestSite/uSync/v8/Content/contact.config create mode 100644 TestSite/uSync/v8/Content/home.config create mode 100644 TestSite/uSync/v8/Content/jan-skovgaard.config create mode 100644 TestSite/uSync/v8/Content/jeavon-leopold.config create mode 100644 TestSite/uSync/v8/Content/jeroen-breuer.config create mode 100644 TestSite/uSync/v8/Content/jumpsuit.config create mode 100644 TestSite/uSync/v8/Content/knitted-west.config create mode 100644 TestSite/uSync/v8/Content/lee-kelleher.config create mode 100644 TestSite/uSync/v8/Content/matt-brailsford.config create mode 100644 TestSite/uSync/v8/Content/my-blog-post.config create mode 100644 TestSite/uSync/v8/Content/people.config create mode 100644 TestSite/uSync/v8/Content/ping-pong-ball.config create mode 100644 TestSite/uSync/v8/Content/products.config create mode 100644 TestSite/uSync/v8/Content/tattoo.config create mode 100644 TestSite/uSync/v8/Content/this-will-be-great.config create mode 100644 TestSite/uSync/v8/Content/todo-list-for-the-starter-kit.config create mode 100644 TestSite/uSync/v8/Content/unicorn.config create mode 100644 TestSite/uSync/v8/ContentTypes/blog.config create mode 100644 TestSite/uSync/v8/ContentTypes/blogpost.config create mode 100644 TestSite/uSync/v8/ContentTypes/contact.config create mode 100644 TestSite/uSync/v8/ContentTypes/contentbase.config create mode 100644 TestSite/uSync/v8/ContentTypes/contentpage.config create mode 100644 TestSite/uSync/v8/ContentTypes/feature.config create mode 100644 TestSite/uSync/v8/ContentTypes/home.config create mode 100644 TestSite/uSync/v8/ContentTypes/navigationbase.config create mode 100644 TestSite/uSync/v8/ContentTypes/people.config create mode 100644 TestSite/uSync/v8/ContentTypes/person.config create mode 100644 TestSite/uSync/v8/ContentTypes/product.config create mode 100644 TestSite/uSync/v8/ContentTypes/products.config create mode 100644 TestSite/uSync/v8/DataTypes/ApprovedColor.config create mode 100644 TestSite/uSync/v8/DataTypes/BlogHowManyPostsShouldBeShownSlider.config create mode 100644 TestSite/uSync/v8/DataTypes/BlogpostCategoriesTags.config create mode 100644 TestSite/uSync/v8/DataTypes/CheckboxList.config create mode 100644 TestSite/uSync/v8/DataTypes/ContactContactIntroRichTextEditor.config create mode 100644 TestSite/uSync/v8/DataTypes/ContactMapCoordinatesOpenStreetMaps.config create mode 100644 TestSite/uSync/v8/DataTypes/ContactPickAContactFormFormPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/ContactPickAContactFormFormPicker1.config create mode 100644 TestSite/uSync/v8/DataTypes/ContentBaseContentGridLayout.config create mode 100644 TestSite/uSync/v8/DataTypes/ContentPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/DatePicker.config create mode 100644 TestSite/uSync/v8/DataTypes/DatePickerWithTime.config create mode 100644 TestSite/uSync/v8/DataTypes/Dropdown.config create mode 100644 TestSite/uSync/v8/DataTypes/DropdownMultiple.config create mode 100644 TestSite/uSync/v8/DataTypes/HomeCallToActionLinkContentPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/HomeColorThemeRadioButtonList.config create mode 100644 TestSite/uSync/v8/DataTypes/HomeContentGridLayout.config create mode 100644 TestSite/uSync/v8/DataTypes/HomeFontRadioButtonList.config create mode 100644 TestSite/uSync/v8/DataTypes/HomeLogoMediaPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/ImageCropper.config create mode 100644 TestSite/uSync/v8/DataTypes/LabelBigint.config create mode 100644 TestSite/uSync/v8/DataTypes/LabelDatetime.config create mode 100644 TestSite/uSync/v8/DataTypes/LabelDecimal.config create mode 100644 TestSite/uSync/v8/DataTypes/LabelInteger.config create mode 100644 TestSite/uSync/v8/DataTypes/LabelString.config create mode 100644 TestSite/uSync/v8/DataTypes/LabelTime.config create mode 100644 TestSite/uSync/v8/DataTypes/ListViewContent.config create mode 100644 TestSite/uSync/v8/DataTypes/ListViewMedia.config create mode 100644 TestSite/uSync/v8/DataTypes/ListViewMembers.config create mode 100644 TestSite/uSync/v8/DataTypes/MediaPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/MemberPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/MultiURLPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/MultipleMediaPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/NavigationBaseKeywordsTags.config create mode 100644 TestSite/uSync/v8/DataTypes/Numeric.config create mode 100644 TestSite/uSync/v8/DataTypes/PeopleFeaturedPeopleMultinodeTreepicker.config create mode 100644 TestSite/uSync/v8/DataTypes/PersonDepartmentTags.config create mode 100644 TestSite/uSync/v8/DataTypes/PersonPhotoMediaPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/ProductCategoryTags.config create mode 100644 TestSite/uSync/v8/DataTypes/ProductFeaturesNestedContent.config create mode 100644 TestSite/uSync/v8/DataTypes/ProductPhotosMediaPicker.config create mode 100644 TestSite/uSync/v8/DataTypes/ProductPriceDecimal.config create mode 100644 TestSite/uSync/v8/DataTypes/ProductsDefaultCurrencyDropdownList.config create mode 100644 TestSite/uSync/v8/DataTypes/ProductsFeaturedProductsMultinodeTreepicker.config create mode 100644 TestSite/uSync/v8/DataTypes/Radiobox.config create mode 100644 TestSite/uSync/v8/DataTypes/RichtextEditor.config create mode 100644 TestSite/uSync/v8/DataTypes/Tags.config create mode 100644 TestSite/uSync/v8/DataTypes/Textarea.config create mode 100644 TestSite/uSync/v8/DataTypes/Textstring.config create mode 100644 TestSite/uSync/v8/DataTypes/TrueFalse.config create mode 100644 TestSite/uSync/v8/DataTypes/Upload.config create mode 100644 TestSite/uSync/v8/Languages/en-us.config create mode 100644 TestSite/uSync/v8/Macros/featuredProduct.config create mode 100644 TestSite/uSync/v8/Macros/latestBlogposts.config create mode 100644 TestSite/uSync/v8/Macros/renderUmbracoForm.config create mode 100644 TestSite/uSync/v8/Media/banjo.config create mode 100644 TestSite/uSync/v8/Media/biker-jacket.config create mode 100644 TestSite/uSync/v8/Media/bowling-ball.config create mode 100644 TestSite/uSync/v8/Media/design.config create mode 100644 TestSite/uSync/v8/Media/jan-skovgaard.config create mode 100644 TestSite/uSync/v8/Media/jeavon-leopold.config create mode 100644 TestSite/uSync/v8/Media/jeroen-breuer.config create mode 100644 TestSite/uSync/v8/Media/jumpsuit.config create mode 100644 TestSite/uSync/v8/Media/knitted-west.config create mode 100644 TestSite/uSync/v8/Media/lee-kelleher.config create mode 100644 TestSite/uSync/v8/Media/matt-brailsford.config create mode 100644 TestSite/uSync/v8/Media/people.config create mode 100644 TestSite/uSync/v8/Media/ping-pong-ball.config create mode 100644 TestSite/uSync/v8/Media/products.config create mode 100644 TestSite/uSync/v8/Media/tattoo.config create mode 100644 TestSite/uSync/v8/Media/umbraco-campari-meeting-room.config create mode 100644 TestSite/uSync/v8/Media/unicorn.config create mode 100644 TestSite/uSync/v8/MediaTypes/file.config create mode 100644 TestSite/uSync/v8/MediaTypes/folder.config create mode 100644 TestSite/uSync/v8/MediaTypes/image.config create mode 100644 TestSite/uSync/v8/Templates/blog.config create mode 100644 TestSite/uSync/v8/Templates/blogpost.config create mode 100644 TestSite/uSync/v8/Templates/contact.config create mode 100644 TestSite/uSync/v8/Templates/contentpage.config create mode 100644 TestSite/uSync/v8/Templates/home.config create mode 100644 TestSite/uSync/v8/Templates/master.config create mode 100644 TestSite/uSync/v8/Templates/people.config create mode 100644 TestSite/uSync/v8/Templates/person.config create mode 100644 TestSite/uSync/v8/Templates/product.config create mode 100644 TestSite/uSync/v8/Templates/products.config create mode 100644 TestSite/uSync/v8/usync.config diff --git a/TestSite/App_Plugins/uSync8/addons.json b/TestSite/App_Plugins/uSync8/addons.json new file mode 100644 index 0000000..65d9f33 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/addons.json @@ -0,0 +1,92 @@ +{ + "intro": "Get more out of uSync", + "banner": { + "icon": "icon icon-infinity", + "title": "uSync Complete", + "headline": "Get everything with uSync.Complete", + "text": "uSync Complete brings together all the best bits of uSync into one place.", + "link": "https://jumoo.co.uk/uSync/complete/", + "icons": [ + { + "icon": "icon-arrow-up", + "name": "publish" + }, + { + "icon": "icon-notepad", + "name": "content" + }, + { + "icon": "icon-split", + "name": "export" + }, + { + "icon": "icon-flash", + "name": "snapshot" + }, + { + "icon": "icon-user", + "name": "people" + }, + { + "icon": "icon-lock", + "name": "audit" + } + ] + }, + "expansions": [ + { + "name": "uSync Content Edition", + "summary": "Add content to your sync", + "icon": "icon-document", + "description": "Add Content, Media, Dictionary Items and Domain Settings to your uSync syncs", + "url": "https://jumoo.co.uk/usync/content/", + "package": "uSync.ContentEdition", + "state": "released" + }, + { + "name": "uSync Snapshots", + "summary": "Moment in time snapshots of your umbraco state", + "icon": "icon-flash", + "description": "Take moment in time snapshots of your umbraco site, and combine and apply them to target sites", + "url": "https://jumoo.co.uk/usync/snapshots/", + "package": "uSync.Snapshots", + "state": "preview" + }, + { + "name": "uSync People Edition", + "summary": "Users and Members", + "icon": "icon-user", + "description": "Sync user and membership accounts", + "url": "https://jumoo.co.uk/usync/people", + "package": "uSync.PeopleEdition", + "state": "preview" + }, + { + "name": "uSync Publisher", + "summary": "Send content and media between servers", + "icon": "icon-arrow-up", + "description": "publisher gives you and your editors and ability to push or pull content between umbraco installations", + "url": "https://jumoo.co.uk/usync/publisher", + "package": "uSync.Publisher", + "state": "preview" + }, + { + "name": "uSync Exporter", + "summary": "Export and Import settings and content in packs", + "icon": "icon-split", + "description": "Exporter lets you build collections of settings, content and media and export them as a sync-pack to be imported on another umbraco installation", + "url": "https://jumoo.co.uk/usync/exporter", + "package": "uSync.Exporter", + "state": "preview" + }, + { + "name": "uSync Audit", + "summary": "Keep track of things", + "icon": "icon-ordered-list", + "description": "Log and audit the changes to your doctypes, datatypes and more, with links to slack", + "url": "https://jumoo.co.uk/usync/audit", + "package": "uSync.Audit", + "state": "working" + } + ] +} \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/backoffice/uSync8/dashboard.html b/TestSite/App_Plugins/uSync8/backoffice/uSync8/dashboard.html new file mode 100644 index 0000000..bc108ce --- /dev/null +++ b/TestSite/App_Plugins/uSync8/backoffice/uSync8/dashboard.html @@ -0,0 +1,14 @@ +
+ + + + + + + +
\ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/backoffice/uSync8/uSyncDashboardController.js b/TestSite/App_Plugins/uSync8/backoffice/uSync8/uSyncDashboardController.js new file mode 100644 index 0000000..e085ec5 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/backoffice/uSync8/uSyncDashboardController.js @@ -0,0 +1,58 @@ +(function () { + 'use strict'; + + function dashboardController( + $scope, $timeout, navigationService, notificationsService, uSync8DashboardService) { + + var vm = this; + + vm.page = { + title: 'uSync 8', + description: '8.1.x', + navigation: [ + { + 'name': 'uSync', + 'alias': 'uSync', + 'icon': 'icon-infinity', + 'view': Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + '/usync8/settings/default.html', + 'active': true + }, + { + 'name': 'Settings', + 'alias': 'settings', + 'icon': 'icon-settings', + 'view': Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + '/uSync8/settings/settings.html' + }, + { + 'name': 'Add ons', + 'alias': 'expansion', + 'icon': 'icon-box', + 'view': Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + '/usync8/settings/expansion.html' + } + ] + }; + + $timeout(function () { + navigationService.syncTree({ tree: "uSync8", path: "-1" }); + }); + + uSync8DashboardService.getAddOns() + .then(function (result) { + vm.addOns = result.data.AddOns; + vm.addOns.forEach(function (value, key) { + if (value.View !== '') { + vm.page.navigation.splice(vm.page.navigation.length - 2, 0, + { + 'name': value.DisplayName, + 'alias': value.Alias, + 'icon': value.Icon, + 'view': value.View + }); + } + }); + }); + } + + angular.module('umbraco') + .controller('uSyncSettingsDashboardController', dashboardController); +})(); \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/changeDialog.html b/TestSite/App_Plugins/uSync8/changeDialog.html new file mode 100644 index 0000000..938a2d7 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/changeDialog.html @@ -0,0 +1,84 @@ +
+ + + + + + + + + +
+
+
+
+
Detected changes
+
+
+
+
+
+
Action
+
Item
+
Difference
+
+
+
+
+
+ + + + +
+
+ {{detail.Change}} +
+
+ {{detail.Name}} +
+
+
{{part.value}}{{part.value}}{{part.value}}
+
+
+
+
+
+
+ +
+

No Changes detected

+

+ It is possible that the .config file is different, because it contains extra information + (such as xml comments) but no properties or values on this item will change when the file + is processed. +

+
+ +
+

Create

+

+ This item doesn't currently exist, and will be created on import. +

+
+
+
+
+
+ + + + + + +
+
\ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/changeDialogController.js b/TestSite/App_Plugins/uSync8/changeDialogController.js new file mode 100644 index 0000000..8124068 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/changeDialogController.js @@ -0,0 +1,57 @@ +(function () { + 'use strict'; + + function changeDialogController($scope, assetsService) { + + var vm = this; + vm.item = $scope.model.item; + + var jsdiff = 'lib/jsdiff/diff.min.js'; + + assetsService.loadJs(jsdiff, $scope).then(function () { + calcDiffs(); + }); + + vm.close = close; + vm.getTypeName = getTypeName; + vm.pageTitle = pageTitle; + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + function getTypeName(typeName) { + var umbType = typeName.substring(0, typeName.indexOf(',')); + return umbType.substring(umbType.lastIndexOf('.') + 1); + } + + function pageTitle() { + return vm.item.Change + ' ' + getTypeName(vm.item.ItemType) + ' ' + vm.item.Name; + } + + function calcDiffs() { + + vm.item.Details.forEach(function (detail, index) { + + + let oldValueDiff = detail.OldValue === null ? "" : detail.OldValue; + let newValueDiff = detail.NewValue === null ? "" : detail.NewValue; + + if (detail.oldValueJson instanceof Object) { + oldValueDiff = JSON.stringify(detail.OldValue, null, 1); + } + + if (detail.newValueJson instanceof Object) { + newValueDiff = JSON.stringify(detail.NewValue, null, 1); + } + + detail.diff = JsDiff.diffWords(oldValueDiff, newValueDiff ); + }); + } + } + + angular.module('umbraco') + .controller('uSyncChangeDialogController', changeDialogController); +})(); \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/components/uSyncProgressView.html b/TestSite/App_Plugins/uSync8/components/uSyncProgressView.html new file mode 100644 index 0000000..6d160a5 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/components/uSyncProgressView.html @@ -0,0 +1,42 @@ +
+
+

{{vm.status.Message}}

+
+
+
+ +
{{handler.Name}}
+
+
{{handler.Changes}}
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
{{vm.update.Message}}
+
+
+
+
+
\ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/components/uSyncReportView.html b/TestSite/App_Plugins/uSync8/components/uSyncReportView.html new file mode 100644 index 0000000..e246a43 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/components/uSyncReportView.html @@ -0,0 +1,87 @@ +
+ +
+
+
{{vm.action}}
+
{{vm.countChanges(vm.results)}} changes across {{vm.results.length}} items
+
+ + +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+ +
+
+ {{vm.getTypeName(result.ItemType)}} +
+
+ {{result.Name}} +
+
+ {{result.Change}} +
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/components/uSyncReportViewComponent.js b/TestSite/App_Plugins/uSync8/components/uSyncReportViewComponent.js new file mode 100644 index 0000000..c2e9b9f --- /dev/null +++ b/TestSite/App_Plugins/uSync8/components/uSyncReportViewComponent.js @@ -0,0 +1,113 @@ +(function () { + 'use strict'; + + var uSyncReportViewComponent = { + templateUrl: Umbraco.Sys.ServerVariables.application.applicationPath + 'App_Plugins/uSync8/Components/uSyncReportView.html', + bindings: { + action: '<', + results: '<', + hideAction: '<', + hideLink: '<', + showAll: '<', + hideToggle: '<' + }, + controllerAs: 'vm', + controller: uSyncReportViewController + }; + + function uSyncReportViewController($scope, editorService, uSync8DashboardService) { + + var vm = this; + + vm.showChange = showChange; + vm.getIcon = getIcon; + vm.getTypeName = getTypeName; + vm.countChanges = countChanges; + vm.openDetail = openDetail; + vm.showAll = vm.showAll || false; + + vm.$onInit = function () { + vm.hideLink = vm.hideLink ? true : false; + vm.hideAction = vm.hideAction ? true : false; + }; + + + vm.apply = apply; + vm.status = status; + + ///////// + + function showChange(change) { + return vm.showAll || (change !== 'NoChange' && change !== 'Removed'); + } + + function getIcon(change) { + switch (change) { + case 'NoChange': + return 'icon-check color-grey'; + case 'Update': + return 'icon-check color-orange'; + case 'Delete': + return 'icon-delete color-red'; + case 'Import': + return 'icon-check color-green'; + case 'Export': + return 'icon-check color-green'; + default: + return 'icon-flag color-red'; + } + } + + function getTypeName(typeName) { + var umbType = typeName.substring(0, typeName.indexOf(',')); + return umbType.substring(umbType.lastIndexOf('.') + 1); + } + + function countChanges(changes) { + var count = 0; + angular.forEach(changes, function (val, key) { + if (val.Change !== 'NoChange') { + count++; + } + }); + + return count; + } + + function openDetail(item) { + + var options = { + item: item, + title: 'uSync Change', + view: Umbraco.Sys.ServerVariables.application.applicationPath + "App_Plugins/uSync8/changeDialog.html", + close: function () { + editorService.close(); + } + }; + editorService.open(options); + } + + function apply(item) { + + // do some application thing (apply just one item) + item.applyState = 'busy'; + uSync8DashboardService.importItem(item) + .then(function (result) { + console.log(result.data); + item.applyState = 'success'; + }, function (error) { + console.log(error); + item.applyState = 'error'; + }); + } + + function status(item) { + if (item.applyState === undefined) return 'init'; + return item.applyState; + } + + } + + angular.module('umbraco') + .component('usyncReportView', uSyncReportViewComponent); +})(); \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/components/usyncProgressViewComponent.js b/TestSite/App_Plugins/uSync8/components/usyncProgressViewComponent.js new file mode 100644 index 0000000..ebebe67 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/components/usyncProgressViewComponent.js @@ -0,0 +1,30 @@ +(function () { + 'use strict'; + + var uSyncProgressViewComponent = { + templateUrl: Umbraco.Sys.ServerVariables.application.applicationPath + 'App_Plugins/uSync8/Components/uSyncProgressView.html', + bindings: { + status: '<', + update: '<', + hideLabels: '<' + }, + controllerAs: 'vm', + controller: uSyncProgressViewController + }; + + function uSyncProgressViewController() { + var vm = this; + + vm.calcPercentage = calcPercentage; + + function calcPercentage(status) { + if (status !== undefined) { + return (100 * status.Count) / status.Total; + } + return 1; + } + } + + angular.module('umbraco') + .component('usyncProgressView', uSyncProgressViewComponent); +})(); \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/lang/en-US.xml b/TestSite/App_Plugins/uSync8/lang/en-US.xml new file mode 100644 index 0000000..7ffdb10 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/lang/en-US.xml @@ -0,0 +1,55 @@ + + + + Jumoo + http://jumoo.uk + + + Synchronization + uSync + + + uSync 8 + Database elements to and from disk + + There is a newer version of uSync available + + + Import + Full Import + Report + Export + Clean Export + Apply + + Details + + Item Handlers + Save Settings + + Import Content/Media + Import Settings + Import Members + Import Users + + Report Content/Media + Report Settings + Report Members + Report Users + + Type + Name + Change + Message + + No Changes]]> + + + + + + + + uSync + + diff --git a/TestSite/App_Plugins/uSync8/lang/nl-NL.xml b/TestSite/App_Plugins/uSync8/lang/nl-NL.xml new file mode 100644 index 0000000..be9f60f --- /dev/null +++ b/TestSite/App_Plugins/uSync8/lang/nl-NL.xml @@ -0,0 +1,51 @@ + + + + Jumoo + http://jumoo.uk + + + Synchronisatie + uSync + + + uSync 8 + Database elementen van en naar disk + + Er is een nieuwere versie van uSync beschikbaar + + + Importeren + Volledige Importeren + Rapporteren + Exporteren + Schoon Exporteren + Toepassen + + Details + + Item Handlers + Instellingen opslaan + + Content/Media Importeren + Instellingen Importeren + Leden Importeren + Gebruikers Importeren + + Content/Media Rapporteren + Instellingen Rapporteren + Leden Rapporteren + Gebruikers Rapporteren + + Type + Naam + Aanpassing + Bericht + + Geen aanpassingen]]> + + + + uSync + + diff --git a/TestSite/App_Plugins/uSync8/package.manifest b/TestSite/App_Plugins/uSync8/package.manifest new file mode 100644 index 0000000..4bf15c6 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/package.manifest @@ -0,0 +1,22 @@ +{ + "javascript": [ + + "~/App_Plugins/uSync8/usync_871.js", + + "~/App_Plugins/uSync8/uSyncService.js", + "~/App_Plugins/uSync8/uSyncHub.js", + + "~/App_Plugins/uSync8/backoffice/uSync8/uSyncDashboardController.js", + "~/App_Plugins/uSync8/settings/settingsController.js", + "~/App_Plugins/uSync8/settings/expansionController.js", + "~/App_Plugins/uSync8/settings/uSyncController.js", + + "~/App_Plugins/uSync8/changeDialogController.js", + "~/App_Plugins/uSync8/components/uSyncReportViewComponent.js", + "~/App_Plugins/uSync8/components/uSyncProgressViewComponent.js" + ], + "css": [ + "~/App_Plugins/uSync8/usync_871.css", + "~/App_Plugins/uSync8/usync.css" + ] +} \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/settings/default.html b/TestSite/App_Plugins/uSync8/settings/default.html new file mode 100644 index 0000000..9c72e09 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/settings/default.html @@ -0,0 +1,83 @@ +
+ + +
+ [v{{vm.versionInfo.VersionInfo.Core}}] + + + {{vm.versionInfo.VersionInfo.Message}} + + +
+ + +
+ +
+ + +
+
+ + + + + + + + + + + + +
+ +
+
{{vm.version}}
+
+ {{vm.savings.title}} {{vm.savings.message}} +
+
+
+ +
+
+ + + + + + + + +
+ {{vm.warnings.Message}} +
+ + + +
+ +
+ +
\ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/settings/expansion.html b/TestSite/App_Plugins/uSync8/settings/expansion.html new file mode 100644 index 0000000..9b687da --- /dev/null +++ b/TestSite/App_Plugins/uSync8/settings/expansion.html @@ -0,0 +1,18 @@ +
+ +
+
+

{{vm.addons.banner.title}}

+

{{vm.addons.banner.text}}

+ + + + Find out more +
+
+
\ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/settings/expansionController.js b/TestSite/App_Plugins/uSync8/settings/expansionController.js new file mode 100644 index 0000000..80e8781 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/settings/expansionController.js @@ -0,0 +1,20 @@ +(function () { + + 'use strict'; + + function expansionController($scope, uSync8DashboardService) { + + var vm = this; + vm.loading = true; + /// + + uSync8DashboardService.getAddOnSplash() + .then(function (result) { + vm.addons = result.data; + vm.loading = false; + }); + } + + angular.module('umbraco') + .controller('uSyncExpansionController', expansionController); +})(); \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/settings/settings.html b/TestSite/App_Plugins/uSync8/settings/settings.html new file mode 100644 index 0000000..8e58c66 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/settings/settings.html @@ -0,0 +1,144 @@ +
+
+
+ + + + + + +
+ + +
+
Import at startup
+
Run an import of files from the disk when Umbraco starts
+
+
+ +
+ + +
+
Export at startup
+
Export the Umbraco settings when the site starts up
+
+
+ + +
+ + +
+
Export on Save
+
Generate uSync files when items are saved
+
+
+ +
+ + +
+
Rebuild Cache after import
+
Request that Umbraco rebuilds its cache after an import
+
+
+ +
+ + +
+
Fail on Missing Parent
+
Import of an item will fail if it's parent is not already in Umbraco or the current Import
+
+
+ +
+
+ + + + +
+

+ If you change these settings, you should perform a 'Clean Export' as they alter the structure of the uSync folders +

+
+
+ + +
+
Flat structure
+
All items of a type are stored in a flat folder structure
+
+
+
+ + +
+
Use Guids for filenames
+
Use the guid of an item as the filename
+
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+ + + Default Set : {{vm.settings.DefaultSet}} + + + + +
+ + + +
+
+ {{handler.Alias}} +
+
+ Enabled for : +
    +
  • {{action}}
+ All +
+
+
+
+
+
+
+
diff --git a/TestSite/App_Plugins/uSync8/settings/settingsController.js b/TestSite/App_Plugins/uSync8/settings/settingsController.js new file mode 100644 index 0000000..343a059 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/settings/settingsController.js @@ -0,0 +1,50 @@ +(function () { + 'use strict'; + + function settingsController($scope, + uSync8DashboardService, + notificationsService) { + + var vm = this; + vm.working = false; + vm.loading = true; + + vm.umbracoVersion = Umbraco.Sys.ServerVariables.application.version; + + vm.saveSettings = saveSettings; + + init(); + + /////////// + + function init() { + getSettings(); + } + + /////////// + function getSettings() { + + uSync8DashboardService.getSettings() + .then(function (result) { + vm.settings = result.data; + vm.loading = false; + }); + } + + + function saveSettings() { + vm.working = false; + uSync8DashboardService.saveSettings(vm.settings) + .then(function (result) { + vm.working = false; + notificationsService.success('Saved', 'Settings updated'); + }, function (error) { + notificationsService.error('Saving', error.data.Message); + }); + } + + } + + angular.module('umbraco') + .controller('uSyncSettingsController', settingsController); +})(); \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/settings/uSyncController.js b/TestSite/App_Plugins/uSync8/settings/uSyncController.js new file mode 100644 index 0000000..4652d5b --- /dev/null +++ b/TestSite/App_Plugins/uSync8/settings/uSyncController.js @@ -0,0 +1,370 @@ +(function () { + 'use strict'; + + function uSyncController($scope, + notificationsService, + editorService, + uSync8DashboardService, + uSyncHub) { + + var vm = this; + vm.loading = true; + vm.working = false; + vm.reported = false; + vm.syncing = false; + vm.hideLink = false; + + vm.showAdvanced = false; + + var modes = { + NONE: 0, + REPORT: 1, + IMPORT: 2, + EXPORT: 3 + }; + + vm.runmode = modes.NONE; + + vm.showAll = false; + vm.status = {}; + vm.reportAction = ''; + + vm.importButton = { + state: 'init', + defaultButton: { + labelKey: 'usync_import', + handler: importItems + }, + subButtons: [{ + labelKey: 'usync_importforce', + handler: importForce + }] + }; + + vm.reportButton = { + state: 'init', + defaultButton: { + labelKey: 'usync_report', + handler: function () { + report(''); + } + }, + subButtons: [] + }; + + vm.exportButton = { + state: 'init', + defaultButton: { + labelKey: 'usync_export', + handler: function () { + exportItems(false); + } + }, + subButtons: [{ + labelKey: 'usync_exportClean', + handler: function () { + exportItems(true); + } + }] + } + + vm.report = report; + vm.versionInfo = { + IsCurrent: true + }; + + vm.exportItems = exportItems; + vm.importForce = importForce; + vm.importItems = importItems; + + vm.getTypeName = getTypeName; + + vm.showChange = showChange; + vm.countChanges = countChanges; + vm.calcPercentage = calcPercentage; + vm.openDetail = openDetail; + + vm.savings = { show: false, title: "", message: "" }; + vm.godo = [ + { time: 0, message: "Worth checking" }, + { time: 180, message: "Go make a cup of tea" }, + { time: 300, message: "Go have a quick chat" }, + { time: 900, message: "Go for a nice walk outside 🚶‍♀️" }, + { time: 3600, message: "You deserve a break" } + ]; + + init(); + + function init() { + InitHub(); + getHandlerGroups(); + + // just so there is something there when you start + uSync8DashboardService.getHandlers() + .then(function (result) { + vm.handlers = result.data; + vm.status.Handlers = vm.handlers; + }); + + uSync8DashboardService.checkVersion() + .then(function (result) { + vm.versionInfo = result.data; + }); + + uSync8DashboardService.getAddOns() + .then(function (result) { + vm.version = 'v' + result.data.Version; + if (result.data.AddOnString.length > 0) { + vm.version += ' + ' + result.data.AddOnString; + } + }); + } + + + + /////////// + function report(group) { + resetStatus(modes.REPORT); + getWarnings('report'); + + uSync8DashboardService.report(group, getClientId()) + .then(function (result) { + vm.results = result.data; + vm.working = false; + vm.reported = true; + }, function (error) { + notificationsService.error('Reporting', error.data.Message); + }); + } + + function exportItems(clean) { + resetStatus(modes.EXPORT); + vm.exportButton.state = 'busy'; + + if (clean && !confirm('Are you sure? A clean export will delete the contents of the uSync folder, you will may loose any delete/rename actions.')) { + vm.working = false; + vm.exportButton.state = 'success'; + return; + } + + vm.hideLink = true; + uSync8DashboardService.exportItems(getClientId(), clean) + .then(function (result) { + vm.results = result.data; + vm.working = false; + vm.reported = true; + vm.exportButton.state = 'success'; + vm.savings.show = true; + vm.savings.title = 'All items exported.'; + vm.savings.message = 'Now go wash your hands 🧼!'; + }, function (error) { + notificationsService.error('Exporting', error.data.ExceptionMessage); + vm.exportButton.state = 'error'; + }); + } + + function importForce() { + importItems(true); + } + + function importItems(force, group) { + resetStatus(modes.IMPORT); + getWarnings('import'); + + vm.hideLink = false; + vm.importButton.state = 'busy'; + + uSync8DashboardService.importItems(force, group, getClientId()) + .then(function (result) { + vm.results = result.data; + vm.working = false; + vm.reported = true; + vm.importButton.state = 'success'; + + calculateTimeSaved(vm.results); + }, function (error) { + vm.importButton.state = 'error'; + notificationsService.error('Failed', error.data.ExceptionMessage); + + vm.working = false; + vm.reported = true; + }); + } + + + // add a little joy to the process. + function calculateTimeSaved(results) { + var changes = countChanges(results); + var time = changes * 26.5; + + var duration = moment.duration(time, 'seconds'); + + if (time >= 180) { + vm.savings.show = true; + vm.savings.title = 'You just saved ' + duration.humanize() + "!"; + vm.savings.message = ''; + + for (let x = 0; x < vm.godo.length; x++) { + if (vm.godo[x].time < time) { + vm.savings.message = vm.godo[x].message; + } + else { + break; + } + } + } + } + + ////////////// + + function getWarnings(action) { + uSync8DashboardService.getSyncWarnings(action) + .then(function (result) { + vm.warnings = result.data; + }); + } + + + function getHandlerGroups() { + uSync8DashboardService.getHandlerGroups() + .then(function (result) { + angular.forEach(result.data, function (group, key) { + vm.importButton.subButtons.push({ + handler: function () { + importGroup(group); + }, + labelKey: 'usync_import-' + group.toLowerCase() + }); + vm.reportButton.subButtons.push({ + handler: function () { + report(group); + }, + labelKey: 'usync_report-' + group.toLowerCase() + }); + }); + vm.loading = false; + }, function (error) { + vm.loading = false; + }); + } + + function importGroup(group) { + importItems(false, group); + } + + ////////////// + + function openDetail(item) { + + var options = { + item: item, + title: 'uSync Change', + view: "/App_Plugins/uSync8/changeDialog.html", + close: function () { + editorService.close(); + } + }; + editorService.open(options); + } + + function getTypeName(typeName) { + var umbType = typeName.substring(0, typeName.indexOf(',')); + return umbType.substring(umbType.lastIndexOf('.') + 1); + } + + function countChanges(changes) { + var count = 0; + angular.forEach(changes, function (val, key) { + if (val.Change !== 'NoChange') { + count++; + } + }); + + return count; + } + + function calcPercentage(status) { + return (100 * status.Count) / status.Total; + } + + function showChange(change) { + return vm.showAll || (change !== 'NoChange' && change !== 'Removed'); + } + + function setFilter(type) { + + if (vm.filter === type) { + vm.filter = ''; + } + else { + vm.filter = type; + } + } + + /////////// + + /// resets all the flags, and messages to the start + function resetStatus(mode) { + vm.warnings = {}; + + vm.reported = vm.showAll = false; + vm.working = true; + vm.runmode = mode; + vm.hideLink = false; + vm.savings.show = false; + + vm.status = { + Count: 0, + Total: 1, + Message: 'Initializing', + Handlers: vm.handlers + }; + + vm.update = { + Message: '', + Count: 0, + Total: 1 + }; + + switch (mode) { + case modes.IMPORT: + vm.action = 'Import'; + break; + case mode.REPORT: + vm.action = 'Report'; + break; + case mode.EXPORT: + vm.action = 'Export'; + break; + } + } + + ////// SignalR things + function InitHub() { + uSyncHub.initHub(function (hub) { + + vm.hub = hub; + + vm.hub.on('add', function (data) { + vm.status = data; + }); + + vm.hub.on('update', function (update) { + vm.update = update; + }); + + vm.hub.start(); + }); + } + + function getClientId() { + if ($.connection !== undefined && $.connection.hub !== undefined) { + return $.connection.hub.id; + } + return ""; + } + } + + angular.module('umbraco') + .controller('uSync8Controller', uSyncController); +})(); \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/uSyncHub.js b/TestSite/App_Plugins/uSync8/uSyncHub.js new file mode 100644 index 0000000..2e0fdbc --- /dev/null +++ b/TestSite/App_Plugins/uSync8/uSyncHub.js @@ -0,0 +1,96 @@ +(function () { + 'use strict'; + + function uSyncHub($rootScope, $q, assetsService) { + + var starting = false; + var callbacks = []; + + var scripts = [ + Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/lib/signalr/jquery.signalR.js', + Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/backoffice/signalr/hubs']; + + var resource = { + initHub: initHub + }; + + return resource; + + ////////////// + + function initHub(callback) { + + callbacks.push(callback); + + if (!starting) { + if ($.connection === undefined) { + starting = true; + // nothing else is using signalR (yet) + // on this site. + // + // You should load it only after a check + // because if you just initialize the + // scripts each time, then when something + // else is using signalR the settings + // will get wiped. + + var promises = []; + scripts.forEach(function (script) { + promises.push(assetsService.loadJs(script)); + }); + + // when everything is loaded setup the hub + $q.all(promises) + .then(function () { + while (callbacks.length) { + var cb = callbacks.pop(); + hubSetup(cb); + } + starting = false; + }); + } + else { + while (callbacks.length) { + var cb = callbacks.pop(); + hubSetup(cb); + } + starting = false; + } + } + } + + function hubSetup(callback) { + var proxy = $.connection.uSyncHub; + + var hub = { + start: function () { + $.connection.hub.start(); + }, + on: function (eventName, callback) { + proxy.on(eventName, function (result) { + $rootScope.$apply(function () { + if (callback) { + callback(result); + } + }); + }); + }, + invoke: function (methodName, callback) { + proxy.invoke(methodName) + .done(function (result) { + $rootScope.$apply(function () { + if (callback) { + callback(result); + } + }); + }); + } + }; + + return callback(hub); + } + } + + angular.module('umbraco.resources') + .factory('uSyncHub', uSyncHub); +})(); \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/uSyncService.js b/TestSite/App_Plugins/uSync8/uSyncService.js new file mode 100644 index 0000000..e895265 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/uSyncService.js @@ -0,0 +1,105 @@ +/** + * @ngdoc + * @name uSync8Service + * @requires $http + * + * @description provides the link to the uSync api elements + * required for the dashboard to function + */ + +(function () { + 'use strict'; + + function uSyncServiceController($http) { + + var serviceRoot = Umbraco.Sys.ServerVariables.uSync.uSyncService; + + var service = { + getSettings: getSettings, + getHandlers: getHandlers, + + report: report, + exportItems: exportItems, + importItems: importItems, + importItem: importItem, + saveSettings: saveSettings, + + getLoadedHandlers: getLoadedHandlers, + getAddOns: getAddOns, + getAddOnSplash: getAddOnSplash, + + getHandlerGroups: getHandlerGroups, + + getSyncWarnings: getSyncWarnings, + + checkVersion: checkVersion + }; + + return service; + + ///////////////////// + + function getSettings() { + return $http.get(serviceRoot + 'GetSettings'); + } + + function getHandlers() { + return $http.get(serviceRoot + 'GetHandlers'); + } + + function getLoadedHandlers() { + return $http.get(serviceRoot + 'GetLoadedHandlers'); + } + + function getAddOns() { + return $http.get(serviceRoot + 'GetAddOns'); + } + + function getAddOnSplash() { + return $http.get(serviceRoot + 'GetAddOnSplash'); + } + + + function report(group, clientId) { + return $http.post(serviceRoot + 'report', { clientId: clientId, group: group }); + } + + function exportItems (clientId, clean) { + return $http.post(serviceRoot + 'export', { clientId: clientId, clean: clean }); + } + + function importItems(force, group, clientId) { + return $http.put(serviceRoot + 'import', + { + force: force, + group: group, + clientId: clientId + }); + } + + function getSyncWarnings(action, group) { + return $http.post(serviceRoot + 'GetSyncWarnings?action=' + action, { group: group }); + } + + + function importItem(item) { + return $http.put(serviceRoot + 'importItem', item); + } + + function saveSettings(settings) { + return $http.post(serviceRoot + 'savesettings', settings); + } + + function getHandlerGroups() { + return $http.get(serviceRoot + 'GetHandlerGroups'); + } + + function checkVersion() { + return $http.get(serviceRoot + 'CheckVersion'); + } + } + + angular.module('umbraco.services') + .factory('uSync8DashboardService', uSyncServiceController); + +})(); \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/uSync_871.js b/TestSite/App_Plugins/uSync8/uSync_871.js new file mode 100644 index 0000000..84be597 --- /dev/null +++ b/TestSite/App_Plugins/uSync8/uSync_871.js @@ -0,0 +1 @@ +// cache breaker. \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/usync.css b/TestSite/App_Plugins/uSync8/usync.css new file mode 100644 index 0000000..412ad5e --- /dev/null +++ b/TestSite/App_Plugins/uSync8/usync.css @@ -0,0 +1,399 @@ +.usync-main { + flex: 1 1 auto; + margin-right: 20px; + width: calc(100% - 370px); +} + +.usync-sidebar { + flex: 0 0 350px; +} + + +.usync-header-buttons { + padding: 1rem 0; +} + + .usync-header-buttons > * { + margin: 0.25rem 0; + } + +.usync-action-buttons { + margin: 0.25rem 0 +} + + .usync-action-buttons > * { + margin-right: 0.25rem; + } + +.usync-action-message { + font-weight: 700; + margin: 0 0 20px; +} + +ul.usync-action-list { + display: inline-block; + margin:0; + padding:0; +} + + ul.usync-action-list li { + display: inline; + margin-right: 4px; + } + +.usync-action-message-step { + margin: 0 0 20px; + font-weight: 700; +} + +.usync-detail-count { + padding: 6px 0; +} + +.usync-item-detail-message { + font-weight: 700; + border-left: 4px solid #ccc; + padding: 5px; +} + +.usync-item-details { + border-left: 4px solid #aaa; +} + + .usync-item-details .umb-table-head .umb-table-row { + background-color: rgba(0,0,0,0.05); + border-bottom: 1px solid black; + } + + .usync-item-details .umb-table { + background-color: #f3f3f5; + } + + .usync-item-details .usync-detail-action-cell { + flex: 0 0 110px; + } + + .usync-item-details .usync-old-value { + text-decoration: line-through; + color: #C62828; + } + + .usync-item-details .usync-new-value { + color: #2e7d32 + } + +.usync-item-details .umb-table-cell { + margin-top: 10px; +} + + .usync-item-details .umb-table-row { + align-items: flex-start; + } + +.usync-row-delete { + background-color: #ffebee; +} + +.usync-row-create { + background-color: #E8F5E9; +} + +.usync-handler-icon { + padding: 0.75em; + margin-right: 14px; + transition: all .3s ease-in; + position: relative; +} + + .usync-handler-icon i { + font-size: 40px; + } + + .usync-handler-icon .handler-badge { + position: absolute; + top: 0; + right: 0; + } + + .usync-handler-icon .badge.type-info { + background-color: #fad634; + } + + .usync-handler-icon .badge.type-complete { + background-color: #27b171; + } + + .usync-handler-icon .handler-badge i { + font-size: 16px; + color: #27b171; + } + + .usync-handler-icon .usync-handler-name { + font-weight: 700; + margin-top: 0.25em; + } + + .usync-handler-icon.enabled { + color: #555 !important; + } + + + .usync-handler-icon.usync-pending { + color: #eee; + } + + .usync-handler-icon.usync-progress { + color: #675e7a; + } + + .usync-handler-icon.usync-complete { + color: #1b264f; + } + .usync-handler-icon.usync-complete .usync-addon-icon { + color: #7b1fa2; + } + + +.usync-settings { + display: flex; +} + + .usync-settings > div { + width: 50%; + } + + .usync-settings .usync-main-settings { + margin-right: 14px; + } + +.usync-root-folder input { + width: 100%; +} + +.usync-root-folder { + display: flex; + padding: 10px 0; + align-items: center; +} + + .usync-root-folder > .controls { + width: 100%; + margin: auto; + } + + .usync-root-folder > .control-label { + width: auto; + white-space: nowrap; + margin-bottom: 0; + } + +.usync-not-animated { + width: calc(100% + 1.5em); + margin: 10px -0.75em; + height: 3px; + border-radius: 0px; +} + + .usync-not-animated .bar { + background-color: #0D47A1; + background-image: none; + } + + .usync-not-animated.usync-complete-changes .bar { + background-color: #27b171; + } + + .usync-not-animated.usync-handler-errors .bar { + background-color: #ff5722; + } + +.usync-not-animated .bar { + transition: none; +} + +.usync-boxes { + display: grid; + grid-gap: 0; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + margin-left: -10px; + margin-right: -10px; +} + +.usync-box { + max-width: 100%; + padding: 10px; + margin: 10px; + box-sizing: border-box; +} + +.usync-box-icon { + font-size: 40px; +} + +.usync-box .umb-box-content { + display: flex; + flex-direction: column; + align-items: center; +} + + .usync-box .umb-box-content > * { + padding: 10px 0; + } + +.usync-box-nuget { + width: 100%; + text-align: left; +} + + .usync-box-nuget a { + background-color: #1b264f; + display: block; + color: #fff; + font-family: monospace; + padding: 10px 10px; + border-radius: 4px; + margin: 10px 0; + } + +.usync-box-inprogress { + opacity: 0.5; +} + +/* umb 8.1 style things */ +.umb-permission { + display: flex; + align-items: center; + margin-bottom: 10px; +} + + .umb-permission > * { + padding-right: 10px; + } + +.umb-permission__description { + font-style: italic; + color: #444; + margin-top: 2px; +} + +.usync-extras { + display: flex; + align-items: center; + justify-content: center; +} + + .usync-extras > div { + padding: 10vh 0; + text-align: center; + font-size: 120%; + } + +.usync-banner { + display: flex; + flex-direction: column; + align-items: center; +} + + .usync-banner h2 { + display: flex; + justify-content: center; + font-weight: 700; + padding: 10px 0; + } + + .usync-banner h2 > i { + padding-right: 15px; + font-size: 60px; + } + + .usync-banner p { + font-size: 120%; + margin: 20px; + line-height: 1.5; + } + + .usync-banner .btn { + margin-top: 40px; + } + +.banner-icons { + display: flex; + width: 80%; + justify-content: space-between; + margin: 25px 0; + font-weight: bold; +} + + .banner-icons > div { + display: flex; + flex-direction: column; + } + + .banner-icons > div > i { + display: block; + padding: 20px; + font-size: 160%; + color: rgba(0,0,0,0.5); + } + +.usync-pane-fade { + margin: -20px; + background: linear-gradient(#e3e3f1, #f6f4f4); +} + +.usync-handler-box .umb-box-header { + cursor: pointer; +} + +.usync-detail-link-cell { + min-width: 120px; +} + +.usync-detail-link-cell .btn { + padding: 6px; +} + +.usync-main-progress { + margin: 0 -20px -20px -20px; +} + +.usync-table-message-cell { + flex-basis: 25%; +} + +.usync-alert a { + text-decoration: underline; +} + +.usync-report-no-changes { + margin: 50px 0; +} +.usync-report-no-changes h4 { + font-weight: 700; +} + +/* diff view */ +.umb-table-cell.usync-diff-value > * { + white-space: pre-wrap; +} + +.umb-table-cell.usync-diff-value pre { + border: none; + padding: 0; + font-family: monospace; + background-color: transparent; +} + + .umb-table-cell.usync-diff-value pre del { + color: red; + } + + .umb-table-cell.usync-diff-value pre ins { + color: green; + } + + + +.usync-change-row-Fail { + background-color: #FFEBEE; +} \ No newline at end of file diff --git a/TestSite/App_Plugins/uSync8/usync_871.css b/TestSite/App_Plugins/uSync8/usync_871.css new file mode 100644 index 0000000..fa2659f --- /dev/null +++ b/TestSite/App_Plugins/uSync8/usync_871.css @@ -0,0 +1 @@ +/* cache breaker */ \ No newline at end of file diff --git a/TestSite/TestSite.csproj b/TestSite/TestSite.csproj index 6a6fe86..cf84ae4 100644 --- a/TestSite/TestSite.csproj +++ b/TestSite/TestSite.csproj @@ -1,4 +1,5 @@  + @@ -273,6 +274,18 @@ ..\packages\UmbracoCms.Web.8.6.3\lib\net472\Umbraco.Web.UI.dll + + ..\packages\uSync.BackOffice.Core.8.7.1\lib\net472\uSync8.BackOffice.dll + + + ..\packages\uSync.Community.Contrib.8.7.1\lib\net472\uSync8.Community.Contrib.dll + + + ..\packages\uSync.ContentEdition.8.7.1\lib\net472\uSync8.ContentEdition.dll + + + ..\packages\uSync.Core.8.7.1\lib\net472\uSync8.Core.dll + @@ -443,6 +456,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -495,6 +529,9 @@ + + + @@ -593,9 +630,12 @@ + + + -
+
-
-
+
+
-
-
-
+
+
+
- - + + - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + - - + + @@ -67,105 +67,105 @@ - - - + + + - - - - + + + + - - - - - + + + + + - + - + - + - - - + + + - - + + - + - + - - - - - - - - - + + + + + + + + + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + - + + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/packages.config b/TestSite/packages.config index f2989f1..7675330 100644 --- a/TestSite/packages.config +++ b/TestSite/packages.config @@ -62,4 +62,9 @@ + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/about-this-starter-kit.config b/TestSite/uSync/v8/Content/about-this-starter-kit.config new file mode 100644 index 0000000..52a183f --- /dev/null +++ b/TestSite/uSync/v8/Content/about-this-starter-kit.config @@ -0,0 +1,41 @@ + + + + About Us + /Home/AboutUs/AboutThisStarterKit + false + contentPage + 2020-07-06T17:22:19 + + 0 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/about-us.config b/TestSite/uSync/v8/Content/about-us.config new file mode 100644 index 0000000..9e770b4 --- /dev/null +++ b/TestSite/uSync/v8/Content/about-us.config @@ -0,0 +1,117 @@ + + + + Home + /Home/AboutUs + false + contentPage + 2020-07-06T17:22:19 + + 2 + + + + + + + Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Curabitur aliquet quam id dui posuere blandit. Vivamus suscipit tortor eget felis porttitor volutpat. Proin eget tortor risus. Sed porttitor lectus nibh. Cras ultricies ligula sed magna dictum porta. Pellentesque in ipsum id orci porta dapibus. Pellentesque in ipsum id orci porta dapibus. Nulla porttitor accumsan tincidunt. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a.

\n

Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Curabitur aliquet quam id dui posuere blandit. Vivamus suscipit tortor eget felis porttitor volutpat. Proin eget tortor risus. Sed porttitor lectus nibh. Cras ultricies ligula sed magna dictum porta. Pellentesque in ipsum id orci porta dapibus. Pellentesque in ipsum id orci porta dapibus. Nulla porttitor accumsan tincidunt. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a.

", + "editor": { + "alias": "rte", + "view": "rte" + }, + "styles": null, + "config": null + }, + { + "value": "", + "editor": { + "alias": "embed", + "view": "embed" + }, + "styles": null, + "config": null + } + ], + "styles": null, + "config": null + } + ], + "styles": null, + "config": null + } + ] + } + ] +}]]>
+
+ + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/TestSite/uSync/v8/Content/another-one.config b/TestSite/uSync/v8/Content/another-one.config new file mode 100644 index 0000000..183e441 --- /dev/null +++ b/TestSite/uSync/v8/Content/another-one.config @@ -0,0 +1,90 @@ + + + + Blog + /Home/Blog/AnotherOne + false + blogpost + 2020-07-06T17:22:19 + + 1 + + + + + + + Donec sollicitudin molestie malesuada. Proin eget tortor risus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Nulla porttitor accumsan tincidunt. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Nulla porttitor accumsan tincidunt. Donec rutrum congue leo eget malesuada.

\n

Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec velit neque, auctor sit amet aliquam vel, ullamcorper sit amet ligula. Pellentesque in ipsum id orci porta dapibus. Donec rutrum congue leo eget malesuada. Nulla porttitor accumsan tincidunt. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec velit neque, auctor sit amet aliquam vel, ullamcorper sit amet ligula. Curabitur non nulla sit amet nisl tempus convallis quis ac lectus. Proin eget tortor risus. Pellentesque in ipsum id orci porta dapibus. Proin eget tortor risus. Sed porttitor lectus nibh.

\n

Pellentesque in ipsum id orci porta dapibus. Curabitur aliquet quam id dui posuere blandit. Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Curabitur non nulla sit amet nisl tempus convallis quis ac lectus. Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Donec rutrum congue leo eget malesuada. Donec rutrum congue leo eget malesuada. Sed porttitor lectus nibh. Nulla quis lorem ut libero malesuada feugiat.

", + "editor": { + "alias": "rte", + "view": null + }, + "styles": null, + "config": null + } + ], + "styles": null, + "config": null + } + ], + "styles": null, + "config": null + } + ] + } + ] +}]]>
+
+ + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/TestSite/uSync/v8/Content/banjo.config b/TestSite/uSync/v8/Content/banjo.config new file mode 100644 index 0000000..a5130af --- /dev/null +++ b/TestSite/uSync/v8/Content/banjo.config @@ -0,0 +1,50 @@ + + + + Products + /Home/Products/Banjo + false + product + 2020-07-06T17:22:19 + + 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/biker-jacket.config b/TestSite/uSync/v8/Content/biker-jacket.config new file mode 100644 index 0000000..0f46291 --- /dev/null +++ b/TestSite/uSync/v8/Content/biker-jacket.config @@ -0,0 +1,72 @@ + + + + Products + /Home/Products/BikerJacket + false + product + 2020-07-06T17:22:19 + + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/blog.config b/TestSite/uSync/v8/Content/blog.config new file mode 100644 index 0000000..d279d2a --- /dev/null +++ b/TestSite/uSync/v8/Content/blog.config @@ -0,0 +1,47 @@ + + + + Home + /Home/Blog + false + blog + 2020-07-06T17:22:19 + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/bowling-ball.config b/TestSite/uSync/v8/Content/bowling-ball.config new file mode 100644 index 0000000..084d03e --- /dev/null +++ b/TestSite/uSync/v8/Content/bowling-ball.config @@ -0,0 +1,50 @@ + + + + Products + /Home/Products/BowlingBall + false + product + 2020-07-06T17:22:19 + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/contact.config b/TestSite/uSync/v8/Content/contact.config new file mode 100644 index 0000000..cc0998b --- /dev/null +++ b/TestSite/uSync/v8/Content/contact.config @@ -0,0 +1,44 @@ + + + + Home + /Home/Contact + false + contact + 2020-07-06T17:22:19 + + 4 + + + + + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam eget lacinia nisl. Aenean sollicitudin diam vitae enim ultrices, semper euismod magna efficitur.

]]>
+
+ + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/TestSite/uSync/v8/Content/home.config b/TestSite/uSync/v8/Content/home.config new file mode 100644 index 0000000..f297b76 --- /dev/null +++ b/TestSite/uSync/v8/Content/home.config @@ -0,0 +1,109 @@ + + + + + /Home + false + home + 2020-07-06T17:22:18 + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/jan-skovgaard.config b/TestSite/uSync/v8/Content/jan-skovgaard.config new file mode 100644 index 0000000..a8a21e4 --- /dev/null +++ b/TestSite/uSync/v8/Content/jan-skovgaard.config @@ -0,0 +1,47 @@ + + + + People + /Home/People/JanSkovgaard + false + person + 2020-07-06T17:22:19 + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/jeavon-leopold.config b/TestSite/uSync/v8/Content/jeavon-leopold.config new file mode 100644 index 0000000..3636595 --- /dev/null +++ b/TestSite/uSync/v8/Content/jeavon-leopold.config @@ -0,0 +1,47 @@ + + + + People + /Home/People/JeavonLeopold + false + person + 2020-07-06T17:22:19 + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/jeroen-breuer.config b/TestSite/uSync/v8/Content/jeroen-breuer.config new file mode 100644 index 0000000..0c98450 --- /dev/null +++ b/TestSite/uSync/v8/Content/jeroen-breuer.config @@ -0,0 +1,47 @@ + + + + People + /Home/People/JeroenBreuer + false + person + 2020-07-06T17:22:19 + + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/jumpsuit.config b/TestSite/uSync/v8/Content/jumpsuit.config new file mode 100644 index 0000000..7dd2e4b --- /dev/null +++ b/TestSite/uSync/v8/Content/jumpsuit.config @@ -0,0 +1,50 @@ + + + + Products + /Home/Products/Jumpsuit + false + product + 2020-07-06T17:22:19 + + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/knitted-west.config b/TestSite/uSync/v8/Content/knitted-west.config new file mode 100644 index 0000000..c54a237 --- /dev/null +++ b/TestSite/uSync/v8/Content/knitted-west.config @@ -0,0 +1,50 @@ + + + + Products + /Home/Products/KnittedWest + false + product + 2020-07-06T17:22:19 + + 6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/lee-kelleher.config b/TestSite/uSync/v8/Content/lee-kelleher.config new file mode 100644 index 0000000..92d0715 --- /dev/null +++ b/TestSite/uSync/v8/Content/lee-kelleher.config @@ -0,0 +1,47 @@ + + + + People + /Home/People/LeeKelleher + false + person + 2020-07-06T17:22:19 + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/matt-brailsford.config b/TestSite/uSync/v8/Content/matt-brailsford.config new file mode 100644 index 0000000..d56bb66 --- /dev/null +++ b/TestSite/uSync/v8/Content/matt-brailsford.config @@ -0,0 +1,47 @@ + + + + People + /Home/People/MattBrailsford + false + person + 2020-07-06T17:22:19 + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/my-blog-post.config b/TestSite/uSync/v8/Content/my-blog-post.config new file mode 100644 index 0000000..2700289 --- /dev/null +++ b/TestSite/uSync/v8/Content/my-blog-post.config @@ -0,0 +1,73 @@ + + + + Blog + /Home/Blog/MyBlogPost + false + blogpost + 2020-07-06T17:22:19 + + 0 + + + + + + + Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Donec sollicitudin molestie malesuada. Vivamus suscipit tortor eget felis porttitor volutpat. Sed porttitor lectus nibh. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Donec sollicitudin molestie malesuada.

\n

Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Donec sollicitudin molestie malesuada. Vivamus suscipit tortor eget felis porttitor volutpat. Sed porttitor lectus nibh. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Donec sollicitudin molestie malesuada.

\n

Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Donec sollicitudin molestie malesuada. Vivamus suscipit tortor eget felis porttitor volutpat. Sed porttitor lectus nibh. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Donec sollicitudin molestie malesuada.

", + "editor": { + "alias": "rte" + }, + "active": true + } + ], + "active": true + } + ], + "hasConfig": false, + "id": "1f96a69f-0cb9-4a00-6a76-f8cec30befec", + "hasActiveChild": true, + "active": true + } + ] + } + ] +}]]>
+
+ + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/TestSite/uSync/v8/Content/people.config b/TestSite/uSync/v8/Content/people.config new file mode 100644 index 0000000..849760c --- /dev/null +++ b/TestSite/uSync/v8/Content/people.config @@ -0,0 +1,44 @@ + + + + Home + /Home/People + false + people + 2020-07-06T17:22:19 + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/ping-pong-ball.config b/TestSite/uSync/v8/Content/ping-pong-ball.config new file mode 100644 index 0000000..0804b93 --- /dev/null +++ b/TestSite/uSync/v8/Content/ping-pong-ball.config @@ -0,0 +1,50 @@ + + + + Products + /Home/Products/PingPongBall + false + product + 2020-07-06T17:22:19 + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/products.config b/TestSite/uSync/v8/Content/products.config new file mode 100644 index 0000000..9f8c3b4 --- /dev/null +++ b/TestSite/uSync/v8/Content/products.config @@ -0,0 +1,47 @@ + + + + Home + /Home/Products + false + products + 2020-07-06T17:22:19 + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/tattoo.config b/TestSite/uSync/v8/Content/tattoo.config new file mode 100644 index 0000000..d157b8a --- /dev/null +++ b/TestSite/uSync/v8/Content/tattoo.config @@ -0,0 +1,50 @@ + + + + Products + /Home/Products/Tattoo + false + product + 2020-07-06T17:22:19 + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Content/this-will-be-great.config b/TestSite/uSync/v8/Content/this-will-be-great.config new file mode 100644 index 0000000..df513d8 --- /dev/null +++ b/TestSite/uSync/v8/Content/this-will-be-great.config @@ -0,0 +1,88 @@ + + + + Blog + /Home/Blog/ThisWillBeGreat + false + blogpost + 2020-07-06T17:22:19 + + 2 + + + + + + + Vivamus suscipit tortor eget felis porttitor volutpat. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Quisque velit nisi, pretium ut lacinia in, elementum id enim. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Proin eget tortor risus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Curabitur non nulla sit amet nisl tempus convallis quis ac lectus. Donec rutrum congue leo eget malesuada. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec velit neque, auctor sit amet aliquam vel, ullamcorper sit amet ligula.

", + "editor": { + "alias": "rte" + }, + "active": false + }, + { + "value": "", + "editor": { + "alias": "embed" + }, + "active": false + }, + { + "value": "

 

\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sollicitudin molestie malesuada. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Cras ultricies ligula sed magna dictum porta. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ultricies ligula sed magna dictum porta. Pellentesque in ipsum id orci porta dapibus.

\n

Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Nulla quis lorem ut libero malesuada feugiat. Curabitur non nulla sit amet nisl tempus convallis quis ac lectus. Sed porttitor lectus nibh. Vivamus suscipit tortor eget felis porttitor volutpat. Nulla porttitor accumsan tincidunt. Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Curabitur non nulla sit amet nisl tempus convallis quis ac lectus. Nulla porttitor accumsan tincidunt.

\n

Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Vivamus suscipit tortor eget felis porttitor volutpat. Sed porttitor lectus nibh. Curabitur non nulla sit amet nisl tempus convallis quis ac lectus. Quisque velit nisi, pretium ut lacinia in, elementum id enim. Donec rutrum congue leo eget malesuada. Nulla porttitor accumsan tincidunt. Nulla quis lorem ut libero malesuada feugiat. Quisque velit nisi, pretium ut lacinia in, elementum id enim. Donec sollicitudin molestie malesuada.

\n

Proin eget tortor risus. Donec rutrum congue leo eget malesuada. Pellentesque in ipsum id orci porta dapibus. Donec rutrum congue leo eget malesuada. Nulla quis lorem ut libero malesuada feugiat. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Donec sollicitudin molestie malesuada. Vivamus suscipit tortor eget felis porttitor volutpat.

", + "editor": { + "alias": "rte" + }, + "active": true + } + ], + "active": true, + "hasActiveChild": true + } + ], + "hasConfig": false, + "id": "4820aba2-8d6b-4a7e-7f57-e0490a9b475e", + "hasActiveChild": true, + "active": true + } + ] + } + ] +}]]>
+
+ + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/TestSite/uSync/v8/Content/todo-list-for-the-starter-kit.config b/TestSite/uSync/v8/Content/todo-list-for-the-starter-kit.config new file mode 100644 index 0000000..053df26 --- /dev/null +++ b/TestSite/uSync/v8/Content/todo-list-for-the-starter-kit.config @@ -0,0 +1,68 @@ + + + + About Us + /Home/AboutUs/TodoListForTheStarterKit + false + contentPage + 2020-07-06T17:22:19 + + 1 + + + + + + + Here's what could be improved in the Starter Kit so far:

\n

 

\n

For v1:

\n
    \n
  • Use a custom grid editor for testimonials
  • \n
  • Integrated Analytics on pages
  • \n
  • Call To Action Button in the grid (with \"Tag Manager\" integration)
  • \n
  • Macro for fetching products (with friendly grid preview)
  • \n
  • Design Review (polish)
  • \n
  • Verify licenses of photos (Niels)
  • \n
\n

For vNext

\n
    \n
  • Swap text with uploaded logo
  • \n
  • Nicer pickers of products and employees
  • \n
  • Custom Listview for products and employees
  • \n
  • Discus template on blog posts
  • \n
  • 404 template
  • \n
  • Member Login/Register/Profile/Forgot password
  • \n
  • Update default styling of grid header
  • \n
  • On a Blog post -> Share/Social (tweet this / facebook this)
  • \n
", + "editor": { + "alias": "rte" + }, + "active": true + } + ], + "active": true, + "hasActiveChild": true + } + ], + "hasConfig": false, + "id": "5ea1e364-8406-e5e6-a82c-45985fd6054e", + "hasActiveChild": true, + "active": true + } + ] + } + ] +}]]>
+
+ + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/TestSite/uSync/v8/Content/unicorn.config b/TestSite/uSync/v8/Content/unicorn.config new file mode 100644 index 0000000..5cbd4e8 --- /dev/null +++ b/TestSite/uSync/v8/Content/unicorn.config @@ -0,0 +1,50 @@ + + + + Products + /Home/Products/Unicorn + false + product + 2020-07-06T17:22:19 + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/blog.config b/TestSite/uSync/v8/ContentTypes/blog.config new file mode 100644 index 0000000..1c15125 --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/blog.config @@ -0,0 +1,62 @@ + + + + Blog + icon-calendar-alt color-black + folder.png + + False + True + Nothing + false + + contentBase + navigationBase + + Blog + + + + + + blogpost + + + + 6eeebe60-5e4a-4ca1-9c42-7d4bd3384f1c + Disqus Shortname + disqusShortname + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 1 + Settings + Nothing + + + + + 1224adc1-f60b-4b51-b397-47dcb3c89f73 + How many posts should be shown? + howManyPostsShouldBeShown + 9d5ba2c5-ed7a-41f8-b454-9fc65f48752e + Umbraco.Slider + true + + + 0 + Settings + Nothing + + + + + + + Settings + 0 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/blogpost.config b/TestSite/uSync/v8/ContentTypes/blogpost.config new file mode 100644 index 0000000..bae8ded --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/blogpost.config @@ -0,0 +1,89 @@ + + + + Blogpost + icon-calendar color-black + folder.png + + False + False + Nothing + false + + navigationBase + + Blogpost + + + + + + + + d1c14dd8-c089-43c7-a801-ae9167ae3cd9 + Content + bodyText + ea0ed4c1-c489-43b8-9058-a70babf430ff + Umbraco.Grid + false + + + 3 + Content + Nothing + + + + + e1252142-4b41-4c90-a8d2-0f24c50f1afb + Categories (tags) + categories + 30c35221-91b0-49a5-a599-42d1965161ea + Umbraco.Tags + false + + + 1 + Content + Nothing + + + + + d45ccd0d-001e-4f93-b75d-c0811cd7c3b5 + Excerpt + excerpt + c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3 + Umbraco.TextArea + true + + + 2 + Content + Nothing + + + + + 39cd719d-6a71-4b62-8304-d050e1de57ab + Page Title + pageTitle + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 0 + Content + Nothing + + + + + + + Content + 0 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/contact.config b/TestSite/uSync/v8/ContentTypes/contact.config new file mode 100644 index 0000000..9940e0a --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/contact.config @@ -0,0 +1,123 @@ + + + + Contact + icon-map-location color-black + folder.png + + False + False + Nothing + false + + navigationBase + + contact + + + + + + + + f904f28d-03c7-45e7-9ff5-cb42f10360bd + Pick a Contact Form + contactForm + a0e0d5ab-372e-405b-a815-a686617c7f3c + UmbracoForms.FormPicker + false + + + 3 + Form + Nothing + + + + + d5802fea-3750-4ac4-8920-c6d8a02015ca + Contact Form Header + contactFormHeader + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 1 + Form + Nothing + + + + + 01d698cf-6d82-4c21-9ee0-194011947d27 + Contact Intro + contactIntro + ecbbac0d-974e-490c-b533-fead926dc525 + Umbraco.TinyMCE + true + + + 2 + Form + Nothing + + + + + a1e46b65-ce8c-46ae-800e-7311ed2c1927 + Map Coordinates + mapCoordinates + fc1475d2-7c7b-4b2f-bc53-54c86fd43d6c + Our.Umbraco.OsmMaps + false + + + 1 + Map + Nothing + + + + + df27a65c-a218-42eb-a8ec-a80e1eaa61bb + Map Header + mapHeader + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 0 + Map + Nothing + + + + + 1ce21aba-6136-40a1-90a8-a07536436db6 + Page Title + pageTitle + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 0 + Form + Nothing + + + + + + + Form + 0 + + + Map + 1 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/contentbase.config b/TestSite/uSync/v8/ContentTypes/contentbase.config new file mode 100644 index 0000000..ca255c2 --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/contentbase.config @@ -0,0 +1,56 @@ + + + + Content Base + icon-document + folder.png + + False + False + Nothing + false + Compositions + + + + + + + + a85b9e30-70f2-4585-9f24-b62bce3e906f + Content + bodyText + ea0ed4c1-c489-43b8-9058-a70babf430ff + Umbraco.Grid + false + + + 1 + Content + Nothing + + + + + 3bff5c50-4455-48bf-abe7-57df740f5c54 + Page Title + pageTitle + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 0 + Content + Nothing + + + + + + + Content + 0 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/contentpage.config b/TestSite/uSync/v8/ContentTypes/contentpage.config new file mode 100644 index 0000000..3fcaea6 --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/contentpage.config @@ -0,0 +1,26 @@ + + + + Content Page + icon-document + folder.png + + False + False + Nothing + false + + contentBase + navigationBase + + contentPage + + + + + + contentPage + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/feature.config b/TestSite/uSync/v8/ContentTypes/feature.config new file mode 100644 index 0000000..ec0883a --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/feature.config @@ -0,0 +1,56 @@ + + + + Feature + icon-plugin color-black + folder.png + + False + False + Nothing + true + Compositions + + + + + + + + 31e0ac02-7a06-4802-8e8e-27c150b0d231 + Details + featureDetails + c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3 + Umbraco.TextArea + false + + + 1 + Feature + Nothing + + + + + 7c204efe-5407-4d67-9354-a96cd2063209 + Name + featureName + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 0 + Feature + Nothing + + + + + + + Feature + 0 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/home.config b/TestSite/uSync/v8/ContentTypes/home.config new file mode 100644 index 0000000..8216236 --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/home.config @@ -0,0 +1,270 @@ + + + + Home + icon-home color-black + folder.png + + True + False + Nothing + false + + home + + + + + + blog + contact + contentPage + people + products + + + + 0f0fe282-f31a-4c58-a5af-d9f7921428b4 + Content + bodyText + 34a7f8f5-a84e-439b-9322-466ff7ed8866 + Umbraco.Grid + false + + + 0 + Content + Nothing + + + + + 7601fd9c-6465-47b0-b234-322be5c59ceb + Color Theme + colorTheme + 71548df5-836c-41f8-96ac-5b98fe5e2e19 + Umbraco.RadioButtonList + true + + + 2 + Design + Nothing + + + + + 9b804660-5400-48e0-bf6b-bb9987fd9f0a + Font + font + fff3c360-3f90-45b3-a554-e29ca72cdce4 + Umbraco.RadioButtonList + true + + + 1 + Design + Nothing + + + + + 888b0b45-a4fd-42f9-b021-007fc5e77109 + Address + footerAddress + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 4 + Footer + Nothing + + + + + df2c34e8-6dd0-47b6-a4bf-5b9eaafe3ecb + Call To Action Caption + footerCTACaption + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 2 + Footer + Nothing + + + + + 8ee89a6a-6cf4-4511-bd48-c0cfeb20ce5c + Call To Action Link + FooterCtalink + 3ece86aa-61ad-45d5-b1dd-8f02f25df949 + Umbraco.ContentPicker + true + + + 3 + Footer + Nothing + + + + + 528bbb99-77fa-48e4-afdf-1e5d0f99c773 + Description + footerDescription + c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3 + Umbraco.TextArea + false + + + 1 + Footer + Nothing + + + + + e2a77977-5992-410d-aadf-2fa8152a73a8 + Header + footerHeader + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 0 + Footer + Nothing + + + + + f229ef4e-4bc8-4b33-8375-d2675867b7b1 + Hero Background + HeroBackgroundImage + 135d60e0-64d9-49ed-ab08-893c9ba44ae5 + Umbraco.MediaPicker + true + + + 0 + Design + Nothing + + + + + f6ee5943-139a-43d9-bfb7-8a649f58e97c + Call To Action Caption + heroCTACaption + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 2 + Hero + Nothing + + + + + 1c707045-3223-4af4-8386-7454440b31c7 + Call To Action Link + HeroCtalink + 3ece86aa-61ad-45d5-b1dd-8f02f25df949 + Umbraco.ContentPicker + true + + + 3 + Hero + Nothing + + + + + bd38b8e2-d064-4088-909a-f55688f52018 + Description + heroDescription + c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3 + Umbraco.TextArea + true + + + 1 + Hero + Nothing + + + + + 2b450973-96d4-497c-9436-9f8820b61be0 + Header + heroHeader + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 0 + Hero + Nothing + + + + + 0b9f2be4-a7fb-4fa1-ba83-5fc405b3d621 + Logo + SiteLogo + 135d60e0-64d9-49ed-ab08-893c9ba44ae5 + Umbraco.MediaPicker + false + + + 4 + Design + Nothing + + + + + abf7b91f-f12d-4bce-bbc8-a16afb6dec65 + Sitename + sitename + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 3 + Design + Nothing + + + + + + + Hero + 0 + + + Content + 1 + + + Footer + 2 + + + Design + 3 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/navigationbase.config b/TestSite/uSync/v8/ContentTypes/navigationbase.config new file mode 100644 index 0000000..a94abb1 --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/navigationbase.config @@ -0,0 +1,71 @@ + + + + Navigation Base + icon-nodes color-black + folder.png + + False + False + Nothing + false + Compositions + + + + + + + + 4c29c8a2-15a2-4b5c-b716-69a71b641b4c + Keywords + keywords + 30c35221-91b0-49a5-a599-42d1965161ea + Umbraco.Tags + false + + + 1 + Navigation & SEO + Nothing + + + + + 74dd8597-2a23-469c-b4e5-0d3b38ff9bf8 + Description + seoMetaDescription + c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3 + Umbraco.TextArea + false + + + 0 + Navigation & SEO + Nothing + + + + + 9565acf2-2469-43cc-8e1b-6dba2d9607d0 + Hide in Navigation + umbracoNavihide + 92897bc6-a5f3-4ffe-ae27-f2e7e33dda49 + Umbraco.TrueFalse + false + + + 2 + Navigation & SEO + Nothing + + + + + + + Navigation & SEO + 0 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/people.config b/TestSite/uSync/v8/ContentTypes/people.config new file mode 100644 index 0000000..7873bda --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/people.config @@ -0,0 +1,47 @@ + + + + People + icon-user-females-alt color-black + folder.png + + False + True + Nothing + false + + contentBase + navigationBase + + people + + + + + + person + + + + be2e45d9-e880-4d18-a0f7-4edf8844ba9f + Featured People + featuredPeople + 4885450e-a60f-42bb-984a-43988baf5283 + Umbraco.MultiNodeTreePicker + false + + + 2 + Content + Nothing + + + + + + + Content + 0 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/person.config b/TestSite/uSync/v8/ContentTypes/person.config new file mode 100644 index 0000000..dc722fe --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/person.config @@ -0,0 +1,138 @@ + + + + Person + icon-user-female color-black + folder.png + + False + False + Nothing + false + + navigationBase + + Person + + + + + + + + eefce912-9ab7-4c64-b7a5-80884a49b099 + Department + department + b58c1530-91c9-4c83-aa35-032aca0f7b89 + Umbraco.Tags + false + + + 1 + Details + Nothing + + + + + 81018753-e8b9-4251-9d5d-29ebfed5a574 + Email + email + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 2 + Details + Nothing + + + + + 3a79adfe-d443-4aa8-bd3d-533d5850180f + Facebook username + facebookUsername + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 1 + Social + Nothing + + + + + daf8889a-98de-4b58-abfa-279f9c4d7061 + Instagram Username + instagramUsername + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 3 + Social + Nothing + + + + + ce8c9c69-c0bd-428f-a97f-a84b7fe63b45 + LinkedIn username + linkedInUsername + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 2 + Social + Nothing + + + + + 0417de76-72d1-4efc-8fd9-e5639dfc4fcc + Photo + photo + e26a8d91-a9d7-475b-bc3b-2a09f4743754 + Umbraco.MediaPicker + false + + + 0 + Details + Nothing + + + + + 7a8b12ad-bd24-4cdd-aacc-454749693554 + Twitter username + twitterUsername + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 0 + Social + Nothing + + + + + + + Details + 0 + + + Social + 0 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/product.config b/TestSite/uSync/v8/ContentTypes/product.config new file mode 100644 index 0000000..7521ec7 --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/product.config @@ -0,0 +1,155 @@ + + + + Product + icon-sweatshirt color-black + folder.png + + False + False + Nothing + false + + Product + + + + + + + + c900954f-b376-4e96-bde3-de5339562945 + Content + bodyText + ea0ed4c1-c489-43b8-9058-a70babf430ff + Umbraco.Grid + false + + + 0 + Detailed Description + Nothing + + + + + 407d832c-51a9-4390-8c50-aed3c0580123 + Category + category + 59ac0a33-9d34-4236-9886-99310d13d7f1 + Umbraco.Tags + true + + + 2 + Product Details + Nothing + + + + + 930c29e3-7d95-4f34-93c4-e072e794e6c1 + Description + description + c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3 + Umbraco.TextArea + true + + + 3 + Product Details + Nothing + + + + + f2277060-cead-4d29-8c93-5874c3bdffbc + Features + features + 84bbeb67-df7e-4043-8db8-55df52d01456 + Umbraco.NestedContent + false + + + 0 + Features + Nothing + + + + + 1e69c013-5e26-4604-88ab-1e096c637ac1 + Photos + photos + 3ada047c-6dfc-4a14-afc4-7efb79390f92 + Umbraco.MediaPicker + true + + + 5 + Product Details + Nothing + + + + + b835da85-dcb0-4195-84ff-828f67aa06e7 + Price + price + 3f6619b6-08a4-4be7-8d6a-2761844ee567 + Umbraco.Decimal + true + + + 1 + Product Details + Nothing + + + + + fe19ab9d-e1a0-41ec-acc8-4d3b85526266 + Product Name + productName + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + true + + + 0 + Product Details + Nothing + + + + + 06b34ebf-9e9c-4ec0-9c16-478ea205c408 + SKU + sku + 0cc0eba1-9960-42c9-bf9b-60e150b429ae + Umbraco.TextBox + false + + + 4 + Product Details + Nothing + + + + + + + Product Details + 0 + + + Features + 1 + + + Detailed Description + 2 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/ContentTypes/products.config b/TestSite/uSync/v8/ContentTypes/products.config new file mode 100644 index 0000000..0336dc9 --- /dev/null +++ b/TestSite/uSync/v8/ContentTypes/products.config @@ -0,0 +1,62 @@ + + + + Products + icon-shopping-basket color-black + folder.png + + False + True + Nothing + false + + contentBase + navigationBase + + Products + + + + + + product + + + + 4b1d713d-1ec6-447e-af96-681522c6dce2 + Default Currency + defaultCurrency + 28a6aeda-b3fc-4c4d-926e-607854a07891 + Umbraco.DropDown.Flexible + true + + + 0 + Shop + Nothing + + + + + 460b11d4-c73a-4228-9e08-30528493f02c + Featured Products + featuredProducts + d0382188-119b-49b7-86d3-84119a02645f + Umbraco.MultiNodeTreePicker + false + + + 1 + Shop + Nothing + + + + + + + Shop + 0 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ApprovedColor.config b/TestSite/uSync/v8/DataTypes/ApprovedColor.config new file mode 100644 index 0000000..ca11632 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ApprovedColor.config @@ -0,0 +1,12 @@ + + + + Approved Color + Umbraco.ColorPicker + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/BlogHowManyPostsShouldBeShownSlider.config b/TestSite/uSync/v8/DataTypes/BlogHowManyPostsShouldBeShownSlider.config new file mode 100644 index 0000000..8826a76 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/BlogHowManyPostsShouldBeShownSlider.config @@ -0,0 +1,16 @@ + + + + Blog - How many posts should be shown - Slider + Umbraco.Slider + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/BlogpostCategoriesTags.config b/TestSite/uSync/v8/DataTypes/BlogpostCategoriesTags.config new file mode 100644 index 0000000..efc9c65 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/BlogpostCategoriesTags.config @@ -0,0 +1,13 @@ + + + + Blogpost - Categories - Tags + Umbraco.Tags + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/CheckboxList.config b/TestSite/uSync/v8/DataTypes/CheckboxList.config new file mode 100644 index 0000000..354bf20 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/CheckboxList.config @@ -0,0 +1,11 @@ + + + + Checkbox list + Umbraco.CheckBoxList + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ContactContactIntroRichTextEditor.config b/TestSite/uSync/v8/DataTypes/ContactContactIntroRichTextEditor.config new file mode 100644 index 0000000..9e896f8 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ContactContactIntroRichTextEditor.config @@ -0,0 +1,36 @@ + + + + Contact - Contact Intro - Rich Text Editor + Umbraco.TinyMCE + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ContactMapCoordinatesOpenStreetMaps.config b/TestSite/uSync/v8/DataTypes/ContactMapCoordinatesOpenStreetMaps.config new file mode 100644 index 0000000..adf8da1 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ContactMapCoordinatesOpenStreetMaps.config @@ -0,0 +1,9 @@ + + + + Contact - Map Coordinates - Open street maps + Our.Umbraco.OsmMaps + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ContactPickAContactFormFormPicker.config b/TestSite/uSync/v8/DataTypes/ContactPickAContactFormFormPicker.config new file mode 100644 index 0000000..268bff2 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ContactPickAContactFormFormPicker.config @@ -0,0 +1,11 @@ + + + + Contact - Pick a Contact Form - Form Picker + UmbracoForms.FormPicker + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ContactPickAContactFormFormPicker1.config b/TestSite/uSync/v8/DataTypes/ContactPickAContactFormFormPicker1.config new file mode 100644 index 0000000..eb35231 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ContactPickAContactFormFormPicker1.config @@ -0,0 +1,11 @@ + + + + Contact - Pick a Contact Form - Form Picker (1) + UmbracoForms.FormPicker + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ContentBaseContentGridLayout.config b/TestSite/uSync/v8/DataTypes/ContentBaseContentGridLayout.config new file mode 100644 index 0000000..b0b08ab --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ContentBaseContentGridLayout.config @@ -0,0 +1,96 @@ + + + + Content Base - Content - Grid layout + Umbraco.Grid + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ContentPicker.config b/TestSite/uSync/v8/DataTypes/ContentPicker.config new file mode 100644 index 0000000..233af08 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ContentPicker.config @@ -0,0 +1,13 @@ + + + + Content Picker + Umbraco.ContentPicker + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/DatePicker.config b/TestSite/uSync/v8/DataTypes/DatePicker.config new file mode 100644 index 0000000..3f9c5c2 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/DatePicker.config @@ -0,0 +1,12 @@ + + + + Date Picker + Umbraco.DateTime + Date + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/DatePickerWithTime.config b/TestSite/uSync/v8/DataTypes/DatePickerWithTime.config new file mode 100644 index 0000000..2c289d1 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/DatePickerWithTime.config @@ -0,0 +1,12 @@ + + + + Date Picker with time + Umbraco.DateTime + Date + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/Dropdown.config b/TestSite/uSync/v8/DataTypes/Dropdown.config new file mode 100644 index 0000000..4be476b --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/Dropdown.config @@ -0,0 +1,12 @@ + + + + Dropdown + Umbraco.DropDown.Flexible + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/DropdownMultiple.config b/TestSite/uSync/v8/DataTypes/DropdownMultiple.config new file mode 100644 index 0000000..1407630 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/DropdownMultiple.config @@ -0,0 +1,12 @@ + + + + Dropdown multiple + Umbraco.DropDown.Flexible + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/HomeCallToActionLinkContentPicker.config b/TestSite/uSync/v8/DataTypes/HomeCallToActionLinkContentPicker.config new file mode 100644 index 0000000..ab84110 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/HomeCallToActionLinkContentPicker.config @@ -0,0 +1,13 @@ + + + + Home - Call To Action Link - Content Picker + Umbraco.ContentPicker + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/HomeColorThemeRadioButtonList.config b/TestSite/uSync/v8/DataTypes/HomeColorThemeRadioButtonList.config new file mode 100644 index 0000000..77d9dae --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/HomeColorThemeRadioButtonList.config @@ -0,0 +1,24 @@ + + + + Home - Color Theme - Radio button list + Umbraco.RadioButtonList + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/HomeContentGridLayout.config b/TestSite/uSync/v8/DataTypes/HomeContentGridLayout.config new file mode 100644 index 0000000..f6dba9f --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/HomeContentGridLayout.config @@ -0,0 +1,117 @@ + + + + Home - Content - Grid layout + Umbraco.Grid + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/HomeFontRadioButtonList.config b/TestSite/uSync/v8/DataTypes/HomeFontRadioButtonList.config new file mode 100644 index 0000000..3d17860 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/HomeFontRadioButtonList.config @@ -0,0 +1,24 @@ + + + + Home - Font - Radio button list + Umbraco.RadioButtonList + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/HomeLogoMediaPicker.config b/TestSite/uSync/v8/DataTypes/HomeLogoMediaPicker.config new file mode 100644 index 0000000..d645161 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/HomeLogoMediaPicker.config @@ -0,0 +1,15 @@ + + + + Home - Logo - Media Picker + Umbraco.MediaPicker + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ImageCropper.config b/TestSite/uSync/v8/DataTypes/ImageCropper.config new file mode 100644 index 0000000..ccd9162 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ImageCropper.config @@ -0,0 +1,11 @@ + + + + Image Cropper + Umbraco.ImageCropper + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/LabelBigint.config b/TestSite/uSync/v8/DataTypes/LabelBigint.config new file mode 100644 index 0000000..909f50d --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/LabelBigint.config @@ -0,0 +1,11 @@ + + + + Label (bigint) + Umbraco.Label + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/LabelDatetime.config b/TestSite/uSync/v8/DataTypes/LabelDatetime.config new file mode 100644 index 0000000..7163ac9 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/LabelDatetime.config @@ -0,0 +1,11 @@ + + + + Label (datetime) + Umbraco.Label + Date + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/LabelDecimal.config b/TestSite/uSync/v8/DataTypes/LabelDecimal.config new file mode 100644 index 0000000..045b4f5 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/LabelDecimal.config @@ -0,0 +1,11 @@ + + + + Label (decimal) + Umbraco.Label + Decimal + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/LabelInteger.config b/TestSite/uSync/v8/DataTypes/LabelInteger.config new file mode 100644 index 0000000..19ea1dc --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/LabelInteger.config @@ -0,0 +1,11 @@ + + + + Label (integer) + Umbraco.Label + Integer + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/LabelString.config b/TestSite/uSync/v8/DataTypes/LabelString.config new file mode 100644 index 0000000..af5067c --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/LabelString.config @@ -0,0 +1,11 @@ + + + + Label (string) + Umbraco.Label + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/LabelTime.config b/TestSite/uSync/v8/DataTypes/LabelTime.config new file mode 100644 index 0000000..df2170a --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/LabelTime.config @@ -0,0 +1,11 @@ + + + + Label (time) + Umbraco.Label + Date + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ListViewContent.config b/TestSite/uSync/v8/DataTypes/ListViewContent.config new file mode 100644 index 0000000..74577a9 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ListViewContent.config @@ -0,0 +1,53 @@ + + + + List View - Content + Umbraco.ListView + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ListViewMedia.config b/TestSite/uSync/v8/DataTypes/ListViewMedia.config new file mode 100644 index 0000000..80f7b22 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ListViewMedia.config @@ -0,0 +1,53 @@ + + + + List View - Media + Umbraco.ListView + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ListViewMembers.config b/TestSite/uSync/v8/DataTypes/ListViewMembers.config new file mode 100644 index 0000000..7640d04 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ListViewMembers.config @@ -0,0 +1,59 @@ + + + + List View - Members + Umbraco.ListView + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/MediaPicker.config b/TestSite/uSync/v8/DataTypes/MediaPicker.config new file mode 100644 index 0000000..62fc396 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/MediaPicker.config @@ -0,0 +1,15 @@ + + + + Media Picker + Umbraco.MediaPicker + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/MemberPicker.config b/TestSite/uSync/v8/DataTypes/MemberPicker.config new file mode 100644 index 0000000..260c185 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/MemberPicker.config @@ -0,0 +1,9 @@ + + + + Member Picker + Umbraco.MemberPicker + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/MultiURLPicker.config b/TestSite/uSync/v8/DataTypes/MultiURLPicker.config new file mode 100644 index 0000000..20ec7a5 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/MultiURLPicker.config @@ -0,0 +1,14 @@ + + + + Multi URL Picker + Umbraco.MultiUrlPicker + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/MultipleMediaPicker.config b/TestSite/uSync/v8/DataTypes/MultipleMediaPicker.config new file mode 100644 index 0000000..06b38fd --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/MultipleMediaPicker.config @@ -0,0 +1,15 @@ + + + + Multiple Media Picker + Umbraco.MediaPicker + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/NavigationBaseKeywordsTags.config b/TestSite/uSync/v8/DataTypes/NavigationBaseKeywordsTags.config new file mode 100644 index 0000000..c36310e --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/NavigationBaseKeywordsTags.config @@ -0,0 +1,13 @@ + + + + Navigation Base - Keywords - Tags + Umbraco.Tags + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/Numeric.config b/TestSite/uSync/v8/DataTypes/Numeric.config new file mode 100644 index 0000000..cd6d29b --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/Numeric.config @@ -0,0 +1,9 @@ + + + + Numeric + Umbraco.Integer + Integer + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/PeopleFeaturedPeopleMultinodeTreepicker.config b/TestSite/uSync/v8/DataTypes/PeopleFeaturedPeopleMultinodeTreepicker.config new file mode 100644 index 0000000..478f8f5 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/PeopleFeaturedPeopleMultinodeTreepicker.config @@ -0,0 +1,16 @@ + + + + People - Featured People - Multinode Treepicker + Umbraco.MultiNodeTreePicker + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/PersonDepartmentTags.config b/TestSite/uSync/v8/DataTypes/PersonDepartmentTags.config new file mode 100644 index 0000000..5d395d7 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/PersonDepartmentTags.config @@ -0,0 +1,13 @@ + + + + Person - Department - Tags + Umbraco.Tags + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/PersonPhotoMediaPicker.config b/TestSite/uSync/v8/DataTypes/PersonPhotoMediaPicker.config new file mode 100644 index 0000000..63f46f0 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/PersonPhotoMediaPicker.config @@ -0,0 +1,15 @@ + + + + Person - Photo - Media Picker + Umbraco.MediaPicker + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ProductCategoryTags.config b/TestSite/uSync/v8/DataTypes/ProductCategoryTags.config new file mode 100644 index 0000000..b59cc16 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ProductCategoryTags.config @@ -0,0 +1,13 @@ + + + + Product - Category - Tags + Umbraco.Tags + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ProductFeaturesNestedContent.config b/TestSite/uSync/v8/DataTypes/ProductFeaturesNestedContent.config new file mode 100644 index 0000000..e4cb49b --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ProductFeaturesNestedContent.config @@ -0,0 +1,22 @@ + + + + Product - Features - Nested Content + Umbraco.NestedContent + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ProductPhotosMediaPicker.config b/TestSite/uSync/v8/DataTypes/ProductPhotosMediaPicker.config new file mode 100644 index 0000000..944703c --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ProductPhotosMediaPicker.config @@ -0,0 +1,15 @@ + + + + Product - Photos - Media Picker + Umbraco.MediaPicker + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ProductPriceDecimal.config b/TestSite/uSync/v8/DataTypes/ProductPriceDecimal.config new file mode 100644 index 0000000..ce42999 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ProductPriceDecimal.config @@ -0,0 +1,9 @@ + + + + Product - Price - Decimal + Umbraco.Decimal + Decimal + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ProductsDefaultCurrencyDropdownList.config b/TestSite/uSync/v8/DataTypes/ProductsDefaultCurrencyDropdownList.config new file mode 100644 index 0000000..f630424 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ProductsDefaultCurrencyDropdownList.config @@ -0,0 +1,29 @@ + + + + Products - Default Currency - Dropdown list + Umbraco.DropDown.Flexible + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/ProductsFeaturedProductsMultinodeTreepicker.config b/TestSite/uSync/v8/DataTypes/ProductsFeaturedProductsMultinodeTreepicker.config new file mode 100644 index 0000000..5c13a13 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/ProductsFeaturedProductsMultinodeTreepicker.config @@ -0,0 +1,16 @@ + + + + Products - Featured Products - Multinode Treepicker + Umbraco.MultiNodeTreePicker + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/Radiobox.config b/TestSite/uSync/v8/DataTypes/Radiobox.config new file mode 100644 index 0000000..a166cb7 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/Radiobox.config @@ -0,0 +1,11 @@ + + + + Radiobox + Umbraco.RadioButtonList + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/RichtextEditor.config b/TestSite/uSync/v8/DataTypes/RichtextEditor.config new file mode 100644 index 0000000..b8d9e11 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/RichtextEditor.config @@ -0,0 +1,14 @@ + + + + Richtext editor + Umbraco.TinyMCE + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/Tags.config b/TestSite/uSync/v8/DataTypes/Tags.config new file mode 100644 index 0000000..fc1025e --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/Tags.config @@ -0,0 +1,13 @@ + + + + Tags + Umbraco.Tags + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/Textarea.config b/TestSite/uSync/v8/DataTypes/Textarea.config new file mode 100644 index 0000000..a495b9e --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/Textarea.config @@ -0,0 +1,12 @@ + + + + Textarea + Umbraco.TextArea + Ntext + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/Textstring.config b/TestSite/uSync/v8/DataTypes/Textstring.config new file mode 100644 index 0000000..dc90bf0 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/Textstring.config @@ -0,0 +1,11 @@ + + + + Textstring + Umbraco.TextBox + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/TrueFalse.config b/TestSite/uSync/v8/DataTypes/TrueFalse.config new file mode 100644 index 0000000..489b5a2 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/TrueFalse.config @@ -0,0 +1,12 @@ + + + + True/false + Umbraco.TrueFalse + Integer + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/DataTypes/Upload.config b/TestSite/uSync/v8/DataTypes/Upload.config new file mode 100644 index 0000000..7ef7279 --- /dev/null +++ b/TestSite/uSync/v8/DataTypes/Upload.config @@ -0,0 +1,9 @@ + + + + Upload + Umbraco.UploadField + Nvarchar + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Languages/en-us.config b/TestSite/uSync/v8/Languages/en-us.config new file mode 100644 index 0000000..c2fe77b --- /dev/null +++ b/TestSite/uSync/v8/Languages/en-us.config @@ -0,0 +1,6 @@ + + + en-US + false + true + \ No newline at end of file diff --git a/TestSite/uSync/v8/Macros/featuredProduct.config b/TestSite/uSync/v8/Macros/featuredProduct.config new file mode 100644 index 0000000..00f7b95 --- /dev/null +++ b/TestSite/uSync/v8/Macros/featuredProduct.config @@ -0,0 +1,19 @@ + + + Select Featured Products + ~/Views/MacroPartials/FeaturedProducts.cshtml + PartialView + true + false + false + false + 0 + + + Choose Product + product + 0 + Umbraco.ContentPicker + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Macros/latestBlogposts.config b/TestSite/uSync/v8/Macros/latestBlogposts.config new file mode 100644 index 0000000..29a726e --- /dev/null +++ b/TestSite/uSync/v8/Macros/latestBlogposts.config @@ -0,0 +1,25 @@ + + + Get Latest Blogposts + ~/Views/MacroPartials/LatestBlogposts.cshtml + PartialView + true + false + false + false + 0 + + + How many posts should be shown + numberOfPosts + 0 + Umbraco.Integer + + + Where to get blog posts from + startNodeId + 1 + Umbraco.ContentPicker + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Macros/renderUmbracoForm.config b/TestSite/uSync/v8/Macros/renderUmbracoForm.config new file mode 100644 index 0000000..0fcd8e4 --- /dev/null +++ b/TestSite/uSync/v8/Macros/renderUmbracoForm.config @@ -0,0 +1,31 @@ + + + Insert Form with Theme + ~/Views/MacroPartials/InsertUmbracoFormWithTheme.cshtml + PartialView + true + false + false + false + 0 + + + Exclude Scripts + ExcludeScripts + 2 + Umbraco.TrueFalse + + + Choose a form + FormGuid + 0 + UmbracoForms.FormPicker + + + Theme + FormTheme + 1 + UmbracoForms.ThemePicker + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/banjo.config b/TestSite/uSync/v8/Media/banjo.config new file mode 100644 index 0000000..481bbd5 --- /dev/null +++ b/TestSite/uSync/v8/Media/banjo.config @@ -0,0 +1,18 @@ + + + + Products + /Products/Banjo + false + Image + 2020-07-06T17:22:22 + + 0 + 1580162923638178247245911192021241154743 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/biker-jacket.config b/TestSite/uSync/v8/Media/biker-jacket.config new file mode 100644 index 0000000..b8f718d --- /dev/null +++ b/TestSite/uSync/v8/Media/biker-jacket.config @@ -0,0 +1,18 @@ + + + + Products + /Products/BikerJacket + false + Image + 2020-07-06T17:22:20 + + 0 + 1993981874888116020747232218226851924 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/bowling-ball.config b/TestSite/uSync/v8/Media/bowling-ball.config new file mode 100644 index 0000000..b9e620d --- /dev/null +++ b/TestSite/uSync/v8/Media/bowling-ball.config @@ -0,0 +1,18 @@ + + + + Products + /Products/BowlingBall + false + Image + 2020-07-06T17:22:21 + + 0 + 9477180168253792309719026552232292332238 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/design.config b/TestSite/uSync/v8/Media/design.config new file mode 100644 index 0000000..466b77a --- /dev/null +++ b/TestSite/uSync/v8/Media/design.config @@ -0,0 +1,14 @@ + + + + + /Design + false + Folder + 2020-07-06T17:22:20 + + 1 + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/jan-skovgaard.config b/TestSite/uSync/v8/Media/jan-skovgaard.config new file mode 100644 index 0000000..5a89712 --- /dev/null +++ b/TestSite/uSync/v8/Media/jan-skovgaard.config @@ -0,0 +1,18 @@ + + + + People + /People/JanSkovgaard + false + Image + 2020-07-06T17:22:22 + + 0 + 0114175271169821511011921524547142144213252 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/jeavon-leopold.config b/TestSite/uSync/v8/Media/jeavon-leopold.config new file mode 100644 index 0000000..4c4bc0d --- /dev/null +++ b/TestSite/uSync/v8/Media/jeavon-leopold.config @@ -0,0 +1,18 @@ + + + + People + /People/JeavonLeopold + false + Image + 2020-07-06T17:22:23 + + 0 + 765875611587925517471486111210240119140 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/jeroen-breuer.config b/TestSite/uSync/v8/Media/jeroen-breuer.config new file mode 100644 index 0000000..60973c0 --- /dev/null +++ b/TestSite/uSync/v8/Media/jeroen-breuer.config @@ -0,0 +1,18 @@ + + + + People + /People/JeroenBreuer + false + Image + 2020-07-06T17:22:23 + + 0 + 113125113111020814813487132127531226525531 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/jumpsuit.config b/TestSite/uSync/v8/Media/jumpsuit.config new file mode 100644 index 0000000..8eaf0f7 --- /dev/null +++ b/TestSite/uSync/v8/Media/jumpsuit.config @@ -0,0 +1,18 @@ + + + + Products + /Products/Jumpsuit + false + Image + 2020-07-06T17:22:21 + + 0 + 18610022815721119522064110147701124814172244 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/knitted-west.config b/TestSite/uSync/v8/Media/knitted-west.config new file mode 100644 index 0000000..6976477 --- /dev/null +++ b/TestSite/uSync/v8/Media/knitted-west.config @@ -0,0 +1,18 @@ + + + + Products + /Products/KnittedWest + false + Image + 2020-07-06T17:22:22 + + 0 + 1691905584601056918982096725417888 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/lee-kelleher.config b/TestSite/uSync/v8/Media/lee-kelleher.config new file mode 100644 index 0000000..2fe5cb0 --- /dev/null +++ b/TestSite/uSync/v8/Media/lee-kelleher.config @@ -0,0 +1,18 @@ + + + + People + /People/LeeKelleher + false + Image + 2020-07-06T17:22:22 + + 0 + 159917583101351161401224821681304568172 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/matt-brailsford.config b/TestSite/uSync/v8/Media/matt-brailsford.config new file mode 100644 index 0000000..3efb57f --- /dev/null +++ b/TestSite/uSync/v8/Media/matt-brailsford.config @@ -0,0 +1,18 @@ + + + + People + /People/MattBrailsford + false + Image + 2020-07-06T17:22:22 + + 0 + 99122152183203931151882232468633646217237 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/people.config b/TestSite/uSync/v8/Media/people.config new file mode 100644 index 0000000..8314917 --- /dev/null +++ b/TestSite/uSync/v8/Media/people.config @@ -0,0 +1,14 @@ + + + + + /People + false + Folder + 2020-07-06T17:22:20 + + 1 + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/ping-pong-ball.config b/TestSite/uSync/v8/Media/ping-pong-ball.config new file mode 100644 index 0000000..208a1a2 --- /dev/null +++ b/TestSite/uSync/v8/Media/ping-pong-ball.config @@ -0,0 +1,18 @@ + + + + Products + /Products/PingPongBall + false + Image + 2020-07-06T17:22:21 + + 0 + 15542351374882242091149520024574230135146 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/products.config b/TestSite/uSync/v8/Media/products.config new file mode 100644 index 0000000..9abced2 --- /dev/null +++ b/TestSite/uSync/v8/Media/products.config @@ -0,0 +1,14 @@ + + + + + /Products + false + Folder + 2020-07-06T17:22:20 + + 1 + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/tattoo.config b/TestSite/uSync/v8/Media/tattoo.config new file mode 100644 index 0000000..33dd378 --- /dev/null +++ b/TestSite/uSync/v8/Media/tattoo.config @@ -0,0 +1,18 @@ + + + + Products + /Products/Tattoo + false + Image + 2020-07-06T17:22:20 + + 0 + 71321841485820626111822135372179225166 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/umbraco-campari-meeting-room.config b/TestSite/uSync/v8/Media/umbraco-campari-meeting-room.config new file mode 100644 index 0000000..6428432 --- /dev/null +++ b/TestSite/uSync/v8/Media/umbraco-campari-meeting-room.config @@ -0,0 +1,18 @@ + + + + Design + /Design/UmbracoCampariMeetingRoom + false + Image + 2020-07-06T17:22:20 + + 0 + 10679217199237208296821512123318421840112164 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Media/unicorn.config b/TestSite/uSync/v8/Media/unicorn.config new file mode 100644 index 0000000..ac96d06 --- /dev/null +++ b/TestSite/uSync/v8/Media/unicorn.config @@ -0,0 +1,18 @@ + + + + Products + /Products/Unicorn + false + Image + 2020-07-06T17:22:21 + + 0 + 602361411241152205684561302294211023847156 + + + + + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/MediaTypes/file.config b/TestSite/uSync/v8/MediaTypes/file.config new file mode 100644 index 0000000..7ed079a --- /dev/null +++ b/TestSite/uSync/v8/MediaTypes/file.config @@ -0,0 +1,65 @@ + + + + File + icon-document + icon-document + + True + False + Nothing + false + + + + + 0000001a-0000-0000-0000-000000000000 + Size + umbracoBytes + 930861bf-e262-4ead-a704-f99453565708 + Umbraco.Label + false + + + 2 + File + + + + + 00000019-0000-0000-0000-000000000000 + Type + umbracoExtension + f0bc4bfb-b499-40d6-ba86-058885a5178c + Umbraco.Label + false + + + 1 + File + + + + + 00000018-0000-0000-0000-000000000000 + Upload file + umbracoFile + 84c6b441-31df-4ffe-b67e-67d5bc3ae65a + Umbraco.UploadField + true + + + 0 + File + + + + + + + + File + 1 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/MediaTypes/folder.config b/TestSite/uSync/v8/MediaTypes/folder.config new file mode 100644 index 0000000..212ee4b --- /dev/null +++ b/TestSite/uSync/v8/MediaTypes/folder.config @@ -0,0 +1,21 @@ + + + + Folder + icon-folder + icon-folder + + True + False + Nothing + false + + + + + Folder + Image + File + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/MediaTypes/image.config b/TestSite/uSync/v8/MediaTypes/image.config new file mode 100644 index 0000000..45bf93e --- /dev/null +++ b/TestSite/uSync/v8/MediaTypes/image.config @@ -0,0 +1,93 @@ + + + + Image + icon-picture + icon-picture + + True + False + Nothing + false + + + + + 00000009-0000-0000-0000-000000000000 + Size + umbracoBytes + 930861bf-e262-4ead-a704-f99453565708 + Umbraco.Label + false + + + 3 + Image + + + + + 0000000a-0000-0000-0000-000000000000 + Type + umbracoExtension + f0bc4bfb-b499-40d6-ba86-058885a5178c + Umbraco.Label + false + + + 4 + Image + + + + + 00000006-0000-0000-0000-000000000000 + Upload image + umbracoFile + 1df9f033-e6d4-451f-b8d2-e0cbc50a836f + Umbraco.ImageCropper + true + + + 0 + Image + + + + + 00000008-0000-0000-0000-000000000000 + Height + umbracoHeight + 8e7f995c-bd81-4627-9932-c40e568ec788 + Umbraco.Label + false + + + 2 + Image + + + + + 00000007-0000-0000-0000-000000000000 + Width + umbracoWidth + 8e7f995c-bd81-4627-9932-c40e568ec788 + Umbraco.Label + false + + + 1 + Image + + + + + + + + Image + 1 + + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/blog.config b/TestSite/uSync/v8/Templates/blog.config new file mode 100644 index 0000000..70b6cd6 --- /dev/null +++ b/TestSite/uSync/v8/Templates/blog.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/blogpost.config b/TestSite/uSync/v8/Templates/blogpost.config new file mode 100644 index 0000000..d0ded9b --- /dev/null +++ b/TestSite/uSync/v8/Templates/blogpost.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/contact.config b/TestSite/uSync/v8/Templates/contact.config new file mode 100644 index 0000000..1f19853 --- /dev/null +++ b/TestSite/uSync/v8/Templates/contact.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/contentpage.config b/TestSite/uSync/v8/Templates/contentpage.config new file mode 100644 index 0000000..92efee6 --- /dev/null +++ b/TestSite/uSync/v8/Templates/contentpage.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/home.config b/TestSite/uSync/v8/Templates/home.config new file mode 100644 index 0000000..132d7f0 --- /dev/null +++ b/TestSite/uSync/v8/Templates/home.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/master.config b/TestSite/uSync/v8/Templates/master.config new file mode 100644 index 0000000..4255553 --- /dev/null +++ b/TestSite/uSync/v8/Templates/master.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/people.config b/TestSite/uSync/v8/Templates/people.config new file mode 100644 index 0000000..cf955e3 --- /dev/null +++ b/TestSite/uSync/v8/Templates/people.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/person.config b/TestSite/uSync/v8/Templates/person.config new file mode 100644 index 0000000..eb8199a --- /dev/null +++ b/TestSite/uSync/v8/Templates/person.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/product.config b/TestSite/uSync/v8/Templates/product.config new file mode 100644 index 0000000..4c21831 --- /dev/null +++ b/TestSite/uSync/v8/Templates/product.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/Templates/products.config b/TestSite/uSync/v8/Templates/products.config new file mode 100644 index 0000000..f02e433 --- /dev/null +++ b/TestSite/uSync/v8/Templates/products.config @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/TestSite/uSync/v8/usync.config b/TestSite/uSync/v8/usync.config new file mode 100644 index 0000000..e577648 --- /dev/null +++ b/TestSite/uSync/v8/usync.config @@ -0,0 +1,4 @@ + + + 2020-10-19T11:26:14 + \ No newline at end of file From 25aa748383e2cf8964b8ef3aef71cd16554d330d Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 14 Sep 2022 20:21:02 +0100 Subject: [PATCH 2/5] Umbraco 8.17.2 --- TestSite/TestSite.csproj | 134 +- TestSite/Umbraco/Config/Lang/cs.xml | 1643 +++- TestSite/Umbraco/Config/Lang/cy.xml | 2764 +++++++ TestSite/Umbraco/Config/Lang/da.xml | 394 +- TestSite/Umbraco/Config/Lang/de.xml | 67 +- TestSite/Umbraco/Config/Lang/en.xml | 437 +- TestSite/Umbraco/Config/Lang/en_us.xml | 413 +- TestSite/Umbraco/Config/Lang/es.xml | 65 +- TestSite/Umbraco/Config/Lang/fr.xml | 695 +- TestSite/Umbraco/Config/Lang/he.xml | 18 +- TestSite/Umbraco/Config/Lang/it.xml | 1679 ++-- TestSite/Umbraco/Config/Lang/ja.xml | 37 +- TestSite/Umbraco/Config/Lang/ko.xml | 22 +- TestSite/Umbraco/Config/Lang/nb.xml | 40 +- TestSite/Umbraco/Config/Lang/nl.xml | 1359 ++- TestSite/Umbraco/Config/Lang/pl.xml | 49 +- TestSite/Umbraco/Config/Lang/pt.xml | 20 +- TestSite/Umbraco/Config/Lang/ru.xml | 55 +- TestSite/Umbraco/Config/Lang/sv.xml | 57 +- TestSite/Umbraco/Config/Lang/tr.xml | 2987 +++++-- TestSite/Umbraco/Config/Lang/zh.xml | 61 +- TestSite/Umbraco/Config/Lang/zh_tw.xml | 39 +- TestSite/Umbraco/Js/app.js | 2 +- TestSite/Umbraco/Js/init.js | 10 +- TestSite/Umbraco/Js/install.loader.js | 8 +- TestSite/Umbraco/Js/main.controller.js | 43 +- TestSite/Umbraco/Js/navigation.controller.js | 28 +- TestSite/Umbraco/Js/routes.js | 44 +- TestSite/Umbraco/Js/umbraco.controllers.js | 6688 ++++++++++----- TestSite/Umbraco/Js/umbraco.directives.js | 7311 +++++++++++++---- TestSite/Umbraco/Js/umbraco.filters.js | 52 +- TestSite/Umbraco/Js/umbraco.installer.js | 26 +- TestSite/Umbraco/Js/umbraco.interceptors.js | 29 +- TestSite/Umbraco/Js/umbraco.preview.js | 185 +- TestSite/Umbraco/Js/umbraco.resources.js | 991 ++- TestSite/Umbraco/Js/umbraco.services.js | 4892 ++++++++--- TestSite/Umbraco/Js/umbraco.websitepreview.js | 121 + TestSite/Umbraco/Js/utilities.js | 139 + .../Templates/Gallery.cshtml | 4 +- .../Umbraco/Views/AuthorizeUpgrade.cshtml | 2 +- TestSite/Umbraco/Views/Default.cshtml | 34 +- TestSite/Umbraco/Views/Preview/Index.cshtml | 27 +- .../Views/common/drawers/help/help.html | 103 +- .../blockeditor/blockeditor.content.html | 1 + .../blockeditor/blockeditor.html | 51 + .../blockeditor/blockeditor.settings.html | 1 + .../blockpicker/blockpicker.html | 84 + .../compositions/compositions.html | 68 +- .../datatypeconfigurationpicker.html | 68 + .../datatypepicker/datatypepicker.html | 180 +- .../datatypesettings/datatypesettings.html | 20 +- .../common/infiniteeditors/embed/embed.html | 19 +- .../iconpicker/iconpicker.html | 27 +- .../insertcodesnippet/insertcodesnippet.html | 44 +- .../insertfield/insertfield.html | 2 +- .../itempicker/itempicker.html | 30 +- .../linkpicker/linkpicker.html | 70 +- .../macroparameterpicker.html | 53 +- .../macropicker/macropicker.html | 38 +- .../mediaentryeditor/mediaentryeditor.html | 127 + .../mediapicker/mediapicker.html | 250 +- .../overlays/mediacropdetails.html | 102 + .../propertysettings/propertysettings.html | 127 +- .../querybuilder/querybuilder.html | 32 +- .../infiniteeditors/rollback/rollback.html | 32 +- .../sectionpicker/sectionpicker.html | 16 +- .../templatesections/templatesections.html | 38 +- .../treepicker/treepicker.html | 26 +- .../usergrouppicker/usergrouppicker.html | 20 +- .../userpicker/userpicker.html | 36 +- .../changepassword/changepassword.html | 6 + .../common/overlays/confirm/confirm.html | 8 +- .../overlays/itempicker/itempicker.html | 51 +- .../logviewersearch/logviewersearch.html | 6 +- .../Views/common/overlays/user/user.html | 61 +- .../umbEmailMarketing/confirm/confirm.html | 15 + .../application/umb-app-header.html | 12 +- .../components/application/umb-backdrop.html | 2 +- .../application/umb-contextmenu.html | 38 +- .../components/application/umb-login.html | 164 +- .../application/umb-navigation.html | 18 +- .../components/application/umb-search.html | 47 +- .../components/application/umb-sections.html | 23 +- .../components/application/umb-tour.html | 2 +- .../application/umbtour/umb-tour-step.html | 4 +- .../components/blockcard/umb-block-card.html | 22 + .../buttons/umb-button-ellipsis.html | 13 + .../components/buttons/umb-button-group.html | 21 +- .../Views/components/buttons/umb-button.html | 17 +- .../components/buttons/umb-toggle-group.html | 3 +- .../Views/components/buttons/umb-toggle.html | 12 +- .../Views/components/content/edit.html | 15 +- .../content/umb-content-node-info.html | 40 +- .../content/umb-tabbed-content.html | 43 +- .../content/umb-variant-content-editors.html | 4 +- .../content/umb-variant-content.html | 20 +- .../contenttype/umb-content-type-group.html | 62 + .../contenttype/umb-content-type-groups.html | 1 + .../umb-content-type-property.html | 149 + .../contenttype/umb-content-type-tab.html | 71 + .../editor/umb-editor-content-header.html | 87 +- .../components/editor/umb-editor-header.html | 28 +- .../components/editor/umb-editor-menu.html | 12 +- .../editor/umb-editor-navigation-item.html | 26 +- .../editor/umb-editor-navigation.html | 48 +- .../editor/umb-editor-sub-view.html | 2 +- .../editor/umb-editor-sub-views.html | 2 +- .../components/editor/umb-editor-tab-bar.html | 3 + .../Views/components/editor/umb-editors.html | 45 +- .../umb-element-editor-content.component.html | 67 + .../Views/components/forms/umb-checkbox.html | 38 +- .../components/forms/umb-radiobutton.html | 34 +- .../components/forms/umb-search-filter.html | 24 + .../components/html/umb-control-group.html | 2 +- .../components/imaging/umb-image-crop.html | 47 +- .../components/imaging/umb-image-gravity.html | 13 +- .../components/media/umb-media-node-info.html | 13 +- .../components/mediacard/umb-media-card.html | 47 + .../member/umb-member-node-info.html | 6 +- .../notifications/umb-notifications.html | 15 +- .../components/overlays/umb-overlay.html | 25 +- .../property/umb-property-actions.html | 20 + .../components/property/umb-property.html | 19 +- .../Views/components/tabs/umb-tabs-nav.html | 31 +- .../components/tags/umb-tags-editor.html | 11 +- .../Views/components/tree/umb-tree-item.html | 23 +- .../components/tree/umb-tree-search-box.html | 21 +- .../tree/umb-tree-search-results.html | 44 +- .../Views/components/tree/umb-tree.html | 80 +- .../umbcontextdialog/umb-context-dialog.html | 12 +- .../Umbraco/Views/components/umb-avatar.html | 4 +- .../Views/components/umb-checkmark.html | 4 +- .../Views/components/umb-child-selector.html | 16 +- .../Views/components/umb-color-swatches.html | 6 +- .../Views/components/umb-confirm-action.html | 18 +- .../Umbraco/Views/components/umb-confirm.html | 38 +- .../Views/components/umb-content-grid.html | 4 +- .../components/umb-date-time-picker.html | 8 +- .../Views/components/umb-file-icon.html | 8 +- .../Views/components/umb-folder-grid.html | 6 +- .../Views/components/umb-generate-alias.html | 2 +- .../Views/components/umb-grid-selector.html | 29 +- .../Views/components/umb-groups-builder.html | 403 +- .../Umbraco/Views/components/umb-icon.html | 8 + .../Views/components/umb-layout-selector.html | 19 +- .../Views/components/umb-lightbox.html | 28 +- .../umb-list-view-settings-overlay.html | 3 +- .../components/umb-list-view-settings.html | 36 +- .../Views/components/umb-locked-field.html | 4 +- .../Views/components/umb-media-grid.html | 106 +- .../Views/components/umb-mini-list-view.html | 45 +- .../Views/components/umb-mini-search.html | 18 + .../Views/components/umb-node-preview.html | 100 +- .../Views/components/umb-pagination.html | 24 +- .../Views/components/umb-progress-bar.html | 2 +- .../umb-property-info-button.html | 9 + .../Umbraco/Views/components/umb-table.html | 24 +- .../components/upload/umb-file-dropzone.html | 171 +- .../upload/umb-property-file-upload.html | 24 +- .../components/users/change-password.html | 15 +- .../users/umb-user-group-preview.html | 8 +- .../components/users/umb-user-preview.html | 23 +- .../Views/content/apps/content/content.html | 6 +- .../Umbraco/Views/content/assigndomain.html | 2 +- TestSite/Umbraco/Views/content/copy.html | 2 +- TestSite/Umbraco/Views/content/create.html | 34 +- .../Views/content/createblueprint.html | 2 +- TestSite/Umbraco/Views/content/edit.html | 1 + .../Views/content/overlays/publish.html | 94 +- .../content/overlays/publishdescendants.html | 84 +- .../Umbraco/Views/content/overlays/save.html | 91 +- .../Views/content/overlays/schedule.html | 98 +- .../Views/content/overlays/sendtopublish.html | 64 +- .../Views/content/overlays/unpublish.html | 71 +- TestSite/Umbraco/Views/content/protect.html | 35 +- .../Umbraco/Views/content/recyclebin.html | 3 +- TestSite/Umbraco/Views/content/rights.html | 10 +- TestSite/Umbraco/Views/content/sort.html | 54 +- .../Views/contentblueprints/create.html | 27 +- .../Umbraco/Views/contentblueprints/edit.html | 3 +- .../Views/contentblueprints/intro.html | 39 +- .../Views/dashboard/content/redirecturls.html | 32 +- .../default/StartupDashboardIntro.html | 19 +- .../default/StartupDashboardVideos.html | 4 +- .../dashboard/forms/formsdashboardintro.html | 2 +- .../dashboard/media/mediadashboardvideos.html | 4 +- .../members/membersdashboardvideos.html | 24 +- .../dashboard/settings/examinemanagement.html | 61 +- .../settings/examinemanagementresults.html | 5 +- .../Views/dashboard/settings/healthcheck.html | 25 +- .../settings/settingsdashboardintro.html | 10 +- .../settings/settingsdashboardvideos.html | 4 +- TestSite/Umbraco/Views/datatypes/create.html | 24 +- TestSite/Umbraco/Views/datatypes/delete.html | 55 +- .../Views/datatypes/views/datatype.info.html | 6 +- TestSite/Umbraco/Views/dictionary/create.html | 8 +- TestSite/Umbraco/Views/dictionary/edit.html | 17 +- TestSite/Umbraco/Views/dictionary/list.html | 28 +- .../Umbraco/Views/documenttypes/copy.html | 2 +- .../Umbraco/Views/documenttypes/create.html | 112 +- .../Umbraco/Views/documenttypes/delete.html | 7 +- .../Umbraco/Views/documenttypes/export.html | 4 +- .../documenttypes/importdocumenttype.html | 6 +- .../Umbraco/Views/documenttypes/move.html | 2 +- .../Umbraco/Views/documenttypes/property.html | 4 +- .../views/listview/listview.html | 4 +- .../views/permissions/permissions.html | 35 +- .../views/templates/templates.html | 4 +- TestSite/Umbraco/Views/errors/BootFailed.html | 79 + TestSite/Umbraco/Views/install/database.html | 205 +- .../Umbraco/Views/install/starterkit.html | 11 +- TestSite/Umbraco/Views/install/upgrade.html | 2 +- .../Umbraco/Views/languages/overview.html | 4 +- .../Umbraco/Views/logviewer/overview.html | 68 +- TestSite/Umbraco/Views/logviewer/search.html | 396 +- .../macros/infiniteeditors/parameter.html | 20 +- .../Views/macros/views/parameters.html | 2 +- .../Umbraco/Views/macros/views/settings.html | 134 +- .../Views/media/apps/content/content.html | 32 +- TestSite/Umbraco/Views/media/create.html | 28 +- TestSite/Umbraco/Views/media/edit.html | 3 +- TestSite/Umbraco/Views/media/recyclebin.html | 3 +- TestSite/Umbraco/Views/media/sort.html | 40 +- TestSite/Umbraco/Views/mediatypes/copy.html | 2 +- TestSite/Umbraco/Views/mediatypes/create.html | 24 +- TestSite/Umbraco/Views/mediatypes/delete.html | 9 +- TestSite/Umbraco/Views/mediatypes/move.html | 2 +- .../mediatypes/views/listview/listview.html | 6 +- .../views/permissions/permissions.html | 8 +- .../Views/member/apps/content/content.html | 32 +- TestSite/Umbraco/Views/member/create.html | 4 +- TestSite/Umbraco/Views/member/delete.html | 2 +- TestSite/Umbraco/Views/member/edit.html | 3 +- TestSite/Umbraco/Views/member/list.html | 3 +- TestSite/Umbraco/Views/membergroups/edit.html | 2 +- TestSite/Umbraco/Views/membertypes/copy.html | 53 + .../Umbraco/Views/membertypes/create.html | 85 +- .../Umbraco/Views/membertypes/delete.html | 7 +- TestSite/Umbraco/Views/packages/edit.html | 117 +- .../Umbraco/Views/packages/views/created.html | 8 +- .../Views/packages/views/install-local.html | 30 +- .../Views/packages/views/installed.html | 12 +- .../Umbraco/Views/packages/views/repo.html | 144 +- .../Views/partialviewmacros/create.html | 20 +- .../Umbraco/Views/partialviews/create.html | 18 +- .../Views/prevalueeditors/checkboxlist.html | 9 + .../Views/prevalueeditors/colorpicker.html | 9 +- .../Umbraco/Views/prevalueeditors/hidden.html | 2 +- .../Views/prevalueeditors/imagepicker.html | 19 +- .../prevalueeditors/mediafolderpicker.html | 24 +- .../Views/prevalueeditors/mediapicker.html | 11 +- .../Views/prevalueeditors/multivalues.html | 8 +- .../Views/prevalueeditors/numberrange.html | 38 + .../Views/prevalueeditors/overlaysize.html | 9 + .../prevalueeditors/radiobuttonlist.html | 38 +- .../Views/prevalueeditors/textstring.html | 2 +- .../Views/prevalueeditors/treepicker.html | 13 +- .../Views/prevalueeditors/treesource.html | 39 +- .../prevalueeditors/treesourcetypepicker.html | 9 +- .../propertyeditors/blocklist/blocklist.html | 1 + .../inlineblock/inlineblock.editor.html | 14 + .../labelblock/labelblock.editor.html | 8 + .../unsupportedblock.editor.html | 15 + .../blocklist.blockconfiguration.html | 23 + .../blocklist.blockconfiguration.overlay.html | 260 + .../umb-block-list-property-editor.html | 79 + .../blocklist/umb-block-list-row.html | 42 + .../propertyeditors/boolean/boolean.html | 24 +- .../checkboxlist/checkboxlist.html | 19 +- .../colorpicker/colorpicker.html | 25 +- .../colorpicker/colorpicker.prevalues.html | 28 +- .../contentpicker/contentpicker.html | 22 +- .../datepicker/datepicker.html | 28 +- .../propertyeditors/decimal/decimal.html | 10 +- .../Views/propertyeditors/email/email.html | 5 +- .../eyedropper/eyedropper.html | 11 + .../fileupload/fileupload.html | 4 +- .../propertyeditors/grid/dialogs/config.html | 15 +- .../grid/dialogs/editconfig.html | 14 +- .../grid/dialogs/layoutconfig.html | 309 +- .../grid/dialogs/rowconfig.html | 233 +- .../propertyeditors/grid/editors/embed.html | 16 +- .../propertyeditors/grid/editors/error.html | 2 +- .../propertyeditors/grid/editors/macro.html | 23 +- .../propertyeditors/grid/editors/media.html | 35 +- .../grid/editors/textstring.html | 8 +- .../Views/propertyeditors/grid/grid.html | 77 +- .../propertyeditors/grid/grid.prevalues.html | 142 +- .../grid/overlays/rowdeleteconfirm.html | 16 + .../imagecropper/imagecropper.html | 28 +- .../imagecropper/imagecropper.prevalues.html | 78 +- .../propertyeditors/integer/integer.html | 9 +- .../listview/icon.prevalues.html | 12 +- .../listview/includeproperties.prevalues.html | 21 +- .../listview/layouts.prevalues.html | 74 +- .../propertyeditors/listview/listview.html | 35 +- .../listview/orderDirection.prevalues.html | 14 +- .../listview/overlays/listviewpublish.html | 4 +- .../listview/overlays/listviewunpublish.html | 4 +- .../markdowneditor/markdowneditor.html | 9 +- .../mediapicker/mediapicker.html | 27 +- .../mediapicker3/mediapicker3.html | 1 + .../prevalue/mediapicker3.crops.html | 96 + .../umb-media-picker3-property-editor.html | 76 + .../membergrouppicker/membergrouppicker.html | 18 +- .../membergroups/membergroups.html | 19 +- .../memberpicker/memberpicker.html | 26 +- .../multipletextbox/multipletextbox.html | 20 +- .../multiurlpicker/multiurlpicker.html | 31 +- .../nestedcontent.doctypepicker.html | 18 +- .../nestedcontent/nestedcontent.editor.html | 20 +- .../nestedcontent.propertyeditor.html | 77 +- .../notsupported/notsupported.html | 3 + .../radiobuttons/radiobuttons.html | 9 +- .../relatedlinks/relatedlinks.html | 22 +- .../Views/propertyeditors/rte/rte.html | 10 +- .../propertyeditors/rte/rte.prevalues.html | 17 +- .../Views/propertyeditors/slider/slider.html | 20 +- .../propertyeditors/textarea/textarea.html | 22 +- .../propertyeditors/textbox/textbox.html | 21 +- .../propertyeditors/urllist/urllist.html | 9 +- .../userpicker/overlays/remove.html | 9 + .../userpicker/userpicker.html | 22 + .../Umbraco/Views/relationtypes/create.html | 35 +- .../Umbraco/Views/relationtypes/edit.html | 1 + .../relationtypes/views/relationType.html | 97 +- TestSite/Umbraco/Views/scripts/create.html | 10 +- .../Umbraco/Views/stylesheets/create.html | 14 +- .../richtextrule/richtextrule.html | 7 +- .../Views/stylesheets/views/rules/rules.html | 10 +- TestSite/Umbraco/Views/templates/edit.html | 30 +- TestSite/Umbraco/Views/users/group.html | 195 +- TestSite/Umbraco/Views/users/user.html | 1 + .../Views/users/views/groups/groups.html | 4 +- .../Views/users/views/overlays/remove.html | 9 + .../Views/users/views/user/details.html | 100 +- .../Views/users/views/users/users.html | 83 +- .../Umbraco/assets/css/canvasdesigner.css | 2 +- TestSite/Umbraco/assets/css/installer.css | 2 +- .../Umbraco/assets/css/nonodes.style.min.css | 2 +- TestSite/Umbraco/assets/css/rte-content.css | 2 +- TestSite/Umbraco/assets/css/umbraco.css | 2 +- .../Umbraco/assets/icons/icon-activity.svg | 1 + TestSite/Umbraco/assets/icons/icon-add.svg | 1 + .../Umbraco/assets/icons/icon-addressbook.svg | 1 + .../Umbraco/assets/icons/icon-alarm-clock.svg | 1 + .../Umbraco/assets/icons/icon-alert-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-alert.svg | 1 + TestSite/Umbraco/assets/icons/icon-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-anchor.svg | 1 + TestSite/Umbraco/assets/icons/icon-app.svg | 1 + .../assets/icons/icon-application-error.svg | 1 + .../icons/icon-application-window-alt.svg | 1 + .../assets/icons/icon-application-window.svg | 1 + .../Umbraco/assets/icons/icon-arrivals.svg | 1 + .../Umbraco/assets/icons/icon-arrow-down.svg | 1 + .../Umbraco/assets/icons/icon-arrow-left.svg | 1 + .../Umbraco/assets/icons/icon-arrow-right.svg | 1 + .../Umbraco/assets/icons/icon-arrow-up.svg | 1 + .../Umbraco/assets/icons/icon-art-easel.svg | 1 + .../Umbraco/assets/icons/icon-article.svg | 1 + .../Umbraco/assets/icons/icon-attachment.svg | 1 + .../assets/icons/icon-auction-hammer.svg | 1 + .../Umbraco/assets/icons/icon-autofill.svg | 1 + TestSite/Umbraco/assets/icons/icon-award.svg | 1 + .../assets/icons/icon-axis-rotation-2.svg | 1 + .../assets/icons/icon-axis-rotation-3.svg | 1 + .../assets/icons/icon-axis-rotation.svg | 1 + .../assets/icons/icon-baby-stroller.svg | 1 + .../Umbraco/assets/icons/icon-backspace.svg | 1 + .../Umbraco/assets/icons/icon-badge-add.svg | 1 + .../Umbraco/assets/icons/icon-badge-count.svg | 1 + .../assets/icons/icon-badge-remove.svg | 1 + .../assets/icons/icon-badge-restricted.svg | 1 + TestSite/Umbraco/assets/icons/icon-ball.svg | 1 + .../Umbraco/assets/icons/icon-band-aid.svg | 1 + .../Umbraco/assets/icons/icon-bar-chart.svg | 1 + .../Umbraco/assets/icons/icon-barcode.svg | 1 + TestSite/Umbraco/assets/icons/icon-bars.svg | 1 + .../assets/icons/icon-battery-full.svg | 1 + .../Umbraco/assets/icons/icon-battery-low.svg | 1 + .../Umbraco/assets/icons/icon-beer-glass.svg | 1 + .../Umbraco/assets/icons/icon-bell-off.svg | 1 + TestSite/Umbraco/assets/icons/icon-bell.svg | 1 + .../Umbraco/assets/icons/icon-bill-dollar.svg | 1 + .../Umbraco/assets/icons/icon-bill-euro.svg | 1 + .../Umbraco/assets/icons/icon-bill-pound.svg | 1 + .../Umbraco/assets/icons/icon-bill-yen.svg | 1 + TestSite/Umbraco/assets/icons/icon-bill.svg | 1 + .../Umbraco/assets/icons/icon-billboard.svg | 1 + .../assets/icons/icon-bills-dollar.svg | 1 + .../Umbraco/assets/icons/icon-bills-euro.svg | 1 + .../Umbraco/assets/icons/icon-bills-pound.svg | 1 + .../Umbraco/assets/icons/icon-bills-yen.svg | 1 + TestSite/Umbraco/assets/icons/icon-bills.svg | 1 + .../Umbraco/assets/icons/icon-binarycode.svg | 1 + .../Umbraco/assets/icons/icon-binoculars.svg | 1 + TestSite/Umbraco/assets/icons/icon-bird.svg | 1 + .../assets/icons/icon-birthday-cake.svg | 1 + TestSite/Umbraco/assets/icons/icon-block.svg | 1 + .../Umbraco/assets/icons/icon-blueprint.svg | 1 + .../Umbraco/assets/icons/icon-bluetooth.svg | 1 + .../assets/icons/icon-boat-shipping.svg | 1 + TestSite/Umbraco/assets/icons/icon-bomb.svg | 1 + TestSite/Umbraco/assets/icons/icon-bones.svg | 1 + .../Umbraco/assets/icons/icon-book-alt-2.svg | 1 + .../Umbraco/assets/icons/icon-book-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-book.svg | 1 + .../Umbraco/assets/icons/icon-bookmark.svg | 1 + TestSite/Umbraco/assets/icons/icon-books.svg | 1 + .../Umbraco/assets/icons/icon-box-alt.svg | 1 + .../Umbraco/assets/icons/icon-box-open.svg | 1 + TestSite/Umbraco/assets/icons/icon-box.svg | 1 + .../Umbraco/assets/icons/icon-brackets.svg | 1 + TestSite/Umbraco/assets/icons/icon-brick.svg | 1 + .../Umbraco/assets/icons/icon-briefcase.svg | 1 + .../assets/icons/icon-browser-window.svg | 1 + .../Umbraco/assets/icons/icon-brush-alt-2.svg | 1 + .../Umbraco/assets/icons/icon-brush-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-brush.svg | 1 + TestSite/Umbraco/assets/icons/icon-bug.svg | 1 + .../assets/icons/icon-bulleted-list.svg | 1 + TestSite/Umbraco/assets/icons/icon-burn.svg | 1 + TestSite/Umbraco/assets/icons/icon-bus.svg | 1 + .../Umbraco/assets/icons/icon-calculator.svg | 1 + .../assets/icons/icon-calendar-alt.svg | 1 + .../Umbraco/assets/icons/icon-calendar.svg | 1 + .../Umbraco/assets/icons/icon-camcorder.svg | 1 + .../Umbraco/assets/icons/icon-camera-roll.svg | 1 + TestSite/Umbraco/assets/icons/icon-candy.svg | 1 + .../Umbraco/assets/icons/icon-caps-lock.svg | 1 + TestSite/Umbraco/assets/icons/icon-car.svg | 1 + .../assets/icons/icon-cash-register.svg | 1 + .../Umbraco/assets/icons/icon-categories.svg | 1 + .../Umbraco/assets/icons/icon-certificate.svg | 1 + .../Umbraco/assets/icons/icon-chart-curve.svg | 1 + TestSite/Umbraco/assets/icons/icon-chart.svg | 1 + .../Umbraco/assets/icons/icon-chat-active.svg | 1 + TestSite/Umbraco/assets/icons/icon-chat.svg | 1 + TestSite/Umbraco/assets/icons/icon-check.svg | 1 + .../icons/icon-checkbox-dotted-active.svg | 1 + .../assets/icons/icon-checkbox-dotted.svg | 1 + .../assets/icons/icon-checkbox-empty.svg | 1 + .../Umbraco/assets/icons/icon-checkbox.svg | 1 + TestSite/Umbraco/assets/icons/icon-chess.svg | 1 + .../Umbraco/assets/icons/icon-chip-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-chip.svg | 1 + TestSite/Umbraco/assets/icons/icon-cinema.svg | 1 + .../icons/icon-circle-dotted-active.svg | 1 + .../assets/icons/icon-circle-dotted.svg | 1 + .../Umbraco/assets/icons/icon-circuits.svg | 1 + TestSite/Umbraco/assets/icons/icon-circus.svg | 1 + TestSite/Umbraco/assets/icons/icon-client.svg | 1 + .../assets/icons/icon-clothes-hanger.svg | 1 + .../Umbraco/assets/icons/icon-cloud-drive.svg | 1 + .../assets/icons/icon-cloud-upload.svg | 1 + TestSite/Umbraco/assets/icons/icon-cloud.svg | 1 + TestSite/Umbraco/assets/icons/icon-cloudy.svg | 1 + TestSite/Umbraco/assets/icons/icon-clubs.svg | 1 + .../Umbraco/assets/icons/icon-cocktail.svg | 1 + TestSite/Umbraco/assets/icons/icon-code.svg | 1 + TestSite/Umbraco/assets/icons/icon-coffee.svg | 1 + .../Umbraco/assets/icons/icon-coin-dollar.svg | 1 + .../Umbraco/assets/icons/icon-coin-euro.svg | 1 + .../Umbraco/assets/icons/icon-coin-pound.svg | 1 + .../Umbraco/assets/icons/icon-coin-yen.svg | 1 + TestSite/Umbraco/assets/icons/icon-coin.svg | 1 + .../Umbraco/assets/icons/icon-coins-alt.svg | 1 + .../assets/icons/icon-coins-dollar-alt.svg | 1 + .../assets/icons/icon-coins-dollar.svg | 1 + .../assets/icons/icon-coins-euro-alt.svg | 1 + .../Umbraco/assets/icons/icon-coins-euro.svg | 1 + .../assets/icons/icon-coins-pound-alt.svg | 1 + .../Umbraco/assets/icons/icon-coins-pound.svg | 1 + .../assets/icons/icon-coins-yen-alt.svg | 1 + .../Umbraco/assets/icons/icon-coins-yen.svg | 1 + TestSite/Umbraco/assets/icons/icon-coins.svg | 1 + .../assets/icons/icon-color-bucket.svg | 1 + .../Umbraco/assets/icons/icon-colorpicker.svg | 1 + .../Umbraco/assets/icons/icon-columns.svg | 1 + TestSite/Umbraco/assets/icons/icon-comb.svg | 1 + .../icons/icon-combination-lock-open.svg | 1 + .../assets/icons/icon-combination-lock.svg | 1 + .../Umbraco/assets/icons/icon-command.svg | 1 + .../Umbraco/assets/icons/icon-company.svg | 1 + .../Umbraco/assets/icons/icon-compress.svg | 1 + .../Umbraco/assets/icons/icon-connection.svg | 1 + .../Umbraco/assets/icons/icon-console.svg | 1 + .../Umbraco/assets/icons/icon-contrast.svg | 1 + .../assets/icons/icon-conversation-alt.svg | 1 + .../assets/icons/icon-conversation.svg | 1 + .../Umbraco/assets/icons/icon-coverflow.svg | 1 + .../assets/icons/icon-credit-card-alt.svg | 1 + .../Umbraco/assets/icons/icon-credit-card.svg | 1 + TestSite/Umbraco/assets/icons/icon-crop.svg | 1 + .../Umbraco/assets/icons/icon-crosshair.svg | 1 + .../Umbraco/assets/icons/icon-crown-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-crown.svg | 1 + .../Umbraco/assets/icons/icon-cupcake.svg | 1 + TestSite/Umbraco/assets/icons/icon-curve.svg | 1 + TestSite/Umbraco/assets/icons/icon-cut.svg | 1 + .../Umbraco/assets/icons/icon-dashboard.svg | 1 + TestSite/Umbraco/assets/icons/icon-defrag.svg | 1 + .../Umbraco/assets/icons/icon-delete-key.svg | 1 + TestSite/Umbraco/assets/icons/icon-delete.svg | 1 + .../Umbraco/assets/icons/icon-departure.svg | 1 + TestSite/Umbraco/assets/icons/icon-desk.svg | 1 + .../Umbraco/assets/icons/icon-desktop.svg | 1 + .../Umbraco/assets/icons/icon-diagnostics.svg | 1 + .../assets/icons/icon-diagonal-arrow-alt.svg | 1 + .../assets/icons/icon-diagonal-arrow.svg | 1 + .../Umbraco/assets/icons/icon-diamond.svg | 1 + .../Umbraco/assets/icons/icon-diamonds.svg | 1 + TestSite/Umbraco/assets/icons/icon-dice.svg | 1 + .../Umbraco/assets/icons/icon-diploma-alt.svg | 1 + .../Umbraco/assets/icons/icon-diploma.svg | 1 + .../assets/icons/icon-directions-alt.svg | 1 + .../Umbraco/assets/icons/icon-directions.svg | 1 + TestSite/Umbraco/assets/icons/icon-disc.svg | 1 + .../Umbraco/assets/icons/icon-disk-image.svg | 1 + .../Umbraco/assets/icons/icon-display.svg | 1 + TestSite/Umbraco/assets/icons/icon-dna.svg | 1 + .../assets/icons/icon-dock-connector.svg | 1 + .../icons/icon-document-dashed-line.svg | 1 + .../Umbraco/assets/icons/icon-document.svg | 1 + .../Umbraco/assets/icons/icon-documents.svg | 1 + .../Umbraco/assets/icons/icon-dollar-bag.svg | 1 + TestSite/Umbraco/assets/icons/icon-donate.svg | 1 + .../assets/icons/icon-door-open-alt.svg | 1 + .../Umbraco/assets/icons/icon-door-open.svg | 1 + .../assets/icons/icon-download-alt.svg | 1 + .../Umbraco/assets/icons/icon-download.svg | 1 + TestSite/Umbraco/assets/icons/icon-drop.svg | 1 + TestSite/Umbraco/assets/icons/icon-eco.svg | 1 + .../Umbraco/assets/icons/icon-economy.svg | 1 + TestSite/Umbraco/assets/icons/icon-edit.svg | 1 + TestSite/Umbraco/assets/icons/icon-eject.svg | 1 + .../Umbraco/assets/icons/icon-employee.svg | 1 + .../assets/icons/icon-energy-saving-bulb.svg | 1 + TestSite/Umbraco/assets/icons/icon-enter.svg | 1 + .../Umbraco/assets/icons/icon-equalizer.svg | 1 + TestSite/Umbraco/assets/icons/icon-escape.svg | 1 + .../Umbraco/assets/icons/icon-ethernet.svg | 1 + .../Umbraco/assets/icons/icon-euro-bag.svg | 1 + .../assets/icons/icon-exit-fullscreen.svg | 1 + TestSite/Umbraco/assets/icons/icon-eye.svg | 1 + .../assets/icons/icon-facebook-like.svg | 1 + .../Umbraco/assets/icons/icon-factory.svg | 1 + .../Umbraco/assets/icons/icon-favorite.svg | 1 + .../assets/icons/icon-female-symbol.svg | 1 + .../assets/icons/icon-file-cabinet.svg | 1 + TestSite/Umbraco/assets/icons/icon-files.svg | 1 + .../assets/icons/icon-filter-arrows.svg | 1 + TestSite/Umbraco/assets/icons/icon-filter.svg | 1 + .../Umbraco/assets/icons/icon-fingerprint.svg | 1 + TestSite/Umbraco/assets/icons/icon-fire.svg | 1 + .../Umbraco/assets/icons/icon-firewall.svg | 1 + .../Umbraco/assets/icons/icon-firewire.svg | 1 + .../Umbraco/assets/icons/icon-flag-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-flag.svg | 1 + TestSite/Umbraco/assets/icons/icon-flash.svg | 1 + .../Umbraco/assets/icons/icon-flashlight.svg | 1 + .../Umbraco/assets/icons/icon-flowerpot.svg | 1 + .../Umbraco/assets/icons/icon-folder-open.svg | 1 + .../assets/icons/icon-folder-outline.svg | 1 + TestSite/Umbraco/assets/icons/icon-folder.svg | 1 + .../Umbraco/assets/icons/icon-folders.svg | 1 + TestSite/Umbraco/assets/icons/icon-font.svg | 1 + TestSite/Umbraco/assets/icons/icon-food.svg | 1 + .../Umbraco/assets/icons/icon-footprints.svg | 1 + .../Umbraco/assets/icons/icon-forking.svg | 1 + .../Umbraco/assets/icons/icon-frame-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-frame.svg | 1 + .../assets/icons/icon-fullscreen-alt.svg | 1 + .../Umbraco/assets/icons/icon-fullscreen.svg | 1 + TestSite/Umbraco/assets/icons/icon-game.svg | 1 + .../Umbraco/assets/icons/icon-geometry.svg | 1 + TestSite/Umbraco/assets/icons/icon-gift.svg | 1 + .../Umbraco/assets/icons/icon-glasses.svg | 1 + .../Umbraco/assets/icons/icon-globe-alt.svg | 1 + .../Umbraco/assets/icons/icon-globe-asia.svg | 1 + .../assets/icons/icon-globe-europe-africa.svg | 1 + .../icons/icon-globe-inverted-america.svg | 1 + .../assets/icons/icon-globe-inverted-asia.svg | 1 + .../icon-globe-inverted-europe-africa.svg | 1 + TestSite/Umbraco/assets/icons/icon-globe.svg | 1 + TestSite/Umbraco/assets/icons/icon-gps.svg | 1 + .../Umbraco/assets/icons/icon-graduate.svg | 1 + TestSite/Umbraco/assets/icons/icon-grid.svg | 1 + TestSite/Umbraco/assets/icons/icon-hammer.svg | 1 + .../assets/icons/icon-hand-active-alt.svg | 1 + .../Umbraco/assets/icons/icon-hand-active.svg | 1 + .../assets/icons/icon-hand-pointer-alt.svg | 1 + .../assets/icons/icon-hand-pointer.svg | 1 + .../Umbraco/assets/icons/icon-handprint.svg | 1 + .../Umbraco/assets/icons/icon-handshake.svg | 1 + .../assets/icons/icon-handtool-alt.svg | 1 + .../Umbraco/assets/icons/icon-handtool.svg | 1 + .../assets/icons/icon-hard-drive-alt.svg | 1 + .../Umbraco/assets/icons/icon-hard-drive.svg | 1 + TestSite/Umbraco/assets/icons/icon-hat.svg | 1 + TestSite/Umbraco/assets/icons/icon-hd.svg | 1 + .../Umbraco/assets/icons/icon-headphones.svg | 1 + .../Umbraco/assets/icons/icon-headset.svg | 1 + TestSite/Umbraco/assets/icons/icon-hearts.svg | 1 + TestSite/Umbraco/assets/icons/icon-height.svg | 1 + .../Umbraco/assets/icons/icon-help-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-help.svg | 1 + TestSite/Umbraco/assets/icons/icon-home.svg | 1 + .../Umbraco/assets/icons/icon-hourglass.svg | 1 + TestSite/Umbraco/assets/icons/icon-imac.svg | 1 + .../assets/icons/icon-inactive-line.svg | 1 + .../Umbraco/assets/icons/icon-inbox-full.svg | 1 + TestSite/Umbraco/assets/icons/icon-inbox.svg | 1 + TestSite/Umbraco/assets/icons/icon-indent.svg | 1 + .../Umbraco/assets/icons/icon-infinity.svg | 1 + TestSite/Umbraco/assets/icons/icon-info.svg | 1 + .../Umbraco/assets/icons/icon-invoice.svg | 1 + TestSite/Umbraco/assets/icons/icon-ipad.svg | 1 + TestSite/Umbraco/assets/icons/icon-iphone.svg | 1 + .../assets/icons/icon-item-arrangement.svg | 1 + TestSite/Umbraco/assets/icons/icon-junk.svg | 1 + TestSite/Umbraco/assets/icons/icon-key.svg | 1 + .../Umbraco/assets/icons/icon-keyboard.svg | 1 + .../Umbraco/assets/icons/icon-keychain.svg | 1 + .../Umbraco/assets/icons/icon-keyhole.svg | 1 + TestSite/Umbraco/assets/icons/icon-lab.svg | 1 + TestSite/Umbraco/assets/icons/icon-laptop.svg | 1 + .../Umbraco/assets/icons/icon-layers-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-layers.svg | 1 + TestSite/Umbraco/assets/icons/icon-layout.svg | 1 + .../assets/icons/icon-left-double-arrow.svg | 1 + TestSite/Umbraco/assets/icons/icon-legal.svg | 1 + TestSite/Umbraco/assets/icons/icon-lense.svg | 1 + .../Umbraco/assets/icons/icon-library.svg | 1 + .../Umbraco/assets/icons/icon-light-down.svg | 1 + .../Umbraco/assets/icons/icon-light-up.svg | 1 + .../assets/icons/icon-lightbulb-active.svg | 1 + .../Umbraco/assets/icons/icon-lightbulb.svg | 1 + .../Umbraco/assets/icons/icon-lightning.svg | 1 + TestSite/Umbraco/assets/icons/icon-link.svg | 1 + .../Umbraco/assets/icons/icon-linux-tux.svg | 1 + TestSite/Umbraco/assets/icons/icon-list.svg | 1 + TestSite/Umbraco/assets/icons/icon-load.svg | 1 + .../Umbraco/assets/icons/icon-loading.svg | 1 + TestSite/Umbraco/assets/icons/icon-locate.svg | 1 + .../assets/icons/icon-location-near-me.svg | 1 + .../assets/icons/icon-location-nearby.svg | 1 + TestSite/Umbraco/assets/icons/icon-lock.svg | 1 + .../Umbraco/assets/icons/icon-log-out.svg | 1 + TestSite/Umbraco/assets/icons/icon-logout.svg | 1 + TestSite/Umbraco/assets/icons/icon-loupe.svg | 1 + TestSite/Umbraco/assets/icons/icon-magnet.svg | 1 + .../Umbraco/assets/icons/icon-mailbox.svg | 1 + .../assets/icons/icon-male-and-female.svg | 1 + .../Umbraco/assets/icons/icon-male-symbol.svg | 1 + .../Umbraco/assets/icons/icon-map-alt.svg | 1 + .../assets/icons/icon-map-location.svg | 1 + .../Umbraco/assets/icons/icon-map-marker.svg | 1 + TestSite/Umbraco/assets/icons/icon-map.svg | 1 + TestSite/Umbraco/assets/icons/icon-medal.svg | 1 + .../assets/icons/icon-medical-emergency.svg | 1 + .../Umbraco/assets/icons/icon-medicine.svg | 1 + .../Umbraco/assets/icons/icon-meeting.svg | 1 + .../Umbraco/assets/icons/icon-megaphone.svg | 1 + TestSite/Umbraco/assets/icons/icon-merge.svg | 1 + .../assets/icons/icon-message-open.svg | 1 + .../assets/icons/icon-message-unopened.svg | 1 + .../Umbraco/assets/icons/icon-message.svg | 1 + .../Umbraco/assets/icons/icon-microscope.svg | 1 + .../Umbraco/assets/icons/icon-mindmap.svg | 1 + TestSite/Umbraco/assets/icons/icon-mobile.svg | 1 + .../assets/icons/icon-molecular-network.svg | 1 + .../Umbraco/assets/icons/icon-molecular.svg | 1 + .../Umbraco/assets/icons/icon-mountain.svg | 1 + .../assets/icons/icon-mouse-cursor.svg | 1 + TestSite/Umbraco/assets/icons/icon-mouse.svg | 1 + .../Umbraco/assets/icons/icon-movie-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-movie.svg | 1 + .../icons/icon-multiple-credit-cards.svg | 1 + .../assets/icons/icon-multiple-windows.svg | 1 + TestSite/Umbraco/assets/icons/icon-music.svg | 1 + .../Umbraco/assets/icons/icon-name-badge.svg | 1 + .../assets/icons/icon-navigation-bottom.svg | 1 + .../assets/icons/icon-navigation-down.svg | 1 + .../assets/icons/icon-navigation-first.svg | 1 + .../icons/icon-navigation-horizontal.svg | 1 + .../assets/icons/icon-navigation-last.svg | 1 + .../assets/icons/icon-navigation-left.svg | 1 + .../assets/icons/icon-navigation-right.svg | 1 + .../assets/icons/icon-navigation-road.svg | 1 + .../assets/icons/icon-navigation-top.svg | 1 + .../assets/icons/icon-navigation-up.svg | 1 + .../assets/icons/icon-navigation-vertical.svg | 1 + .../Umbraco/assets/icons/icon-navigation.svg | 1 + .../assets/icons/icon-navigational-arrow.svg | 1 + .../Umbraco/assets/icons/icon-network-alt.svg | 1 + .../assets/icons/icon-newspaper-alt.svg | 1 + .../Umbraco/assets/icons/icon-newspaper.svg | 1 + .../Umbraco/assets/icons/icon-next-media.svg | 1 + TestSite/Umbraco/assets/icons/icon-next.svg | 1 + TestSite/Umbraco/assets/icons/icon-nodes.svg | 1 + .../Umbraco/assets/icons/icon-notepad-alt.svg | 1 + .../Umbraco/assets/icons/icon-notepad.svg | 1 + .../Umbraco/assets/icons/icon-old-key.svg | 1 + .../Umbraco/assets/icons/icon-old-phone.svg | 1 + .../Umbraco/assets/icons/icon-operator.svg | 1 + .../assets/icons/icon-ordered-list.svg | 1 + TestSite/Umbraco/assets/icons/icon-os-x.svg | 1 + TestSite/Umbraco/assets/icons/icon-out.svg | 1 + TestSite/Umbraco/assets/icons/icon-outbox.svg | 1 + .../Umbraco/assets/icons/icon-outdent.svg | 1 + .../Umbraco/assets/icons/icon-page-add.svg | 1 + .../Umbraco/assets/icons/icon-page-down.svg | 1 + .../Umbraco/assets/icons/icon-page-remove.svg | 1 + .../assets/icons/icon-page-restricted.svg | 1 + .../Umbraco/assets/icons/icon-page-up.svg | 1 + .../assets/icons/icon-paint-roller.svg | 1 + .../Umbraco/assets/icons/icon-palette.svg | 1 + .../Umbraco/assets/icons/icon-panel-show.svg | 1 + .../assets/icons/icon-pannel-close.svg | 1 + TestSite/Umbraco/assets/icons/icon-pants.svg | 1 + .../Umbraco/assets/icons/icon-paper-bag.svg | 1 + .../assets/icons/icon-paper-plane-alt.svg | 1 + .../Umbraco/assets/icons/icon-paper-plane.svg | 1 + .../assets/icons/icon-parachute-drop.svg | 1 + .../assets/icons/icon-parental-control.svg | 1 + .../assets/icons/icon-partly-cloudy.svg | 1 + .../Umbraco/assets/icons/icon-paste-in.svg | 1 + TestSite/Umbraco/assets/icons/icon-path.svg | 1 + TestSite/Umbraco/assets/icons/icon-pause.svg | 1 + TestSite/Umbraco/assets/icons/icon-pc.svg | 1 + .../assets/icons/icon-people-alt-2.svg | 1 + .../Umbraco/assets/icons/icon-people-alt.svg | 1 + .../assets/icons/icon-people-female.svg | 1 + TestSite/Umbraco/assets/icons/icon-people.svg | 1 + .../Umbraco/assets/icons/icon-phone-ring.svg | 1 + TestSite/Umbraco/assets/icons/icon-phone.svg | 1 + .../Umbraco/assets/icons/icon-photo-album.svg | 1 + .../Umbraco/assets/icons/icon-picture.svg | 1 + .../assets/icons/icon-pictures-alt-2.svg | 1 + .../assets/icons/icon-pictures-alt.svg | 1 + .../Umbraco/assets/icons/icon-pictures.svg | 1 + .../Umbraco/assets/icons/icon-pie-chart.svg | 1 + .../Umbraco/assets/icons/icon-piggy-bank.svg | 1 + .../assets/icons/icon-pin-location.svg | 1 + TestSite/Umbraco/assets/icons/icon-piracy.svg | 1 + TestSite/Umbraco/assets/icons/icon-plane.svg | 1 + TestSite/Umbraco/assets/icons/icon-planet.svg | 1 + TestSite/Umbraco/assets/icons/icon-play.svg | 1 + .../assets/icons/icon-playing-cards.svg | 1 + .../Umbraco/assets/icons/icon-playlist.svg | 1 + TestSite/Umbraco/assets/icons/icon-plugin.svg | 1 + .../Umbraco/assets/icons/icon-podcast.svg | 1 + .../Umbraco/assets/icons/icon-poker-chip.svg | 1 + TestSite/Umbraco/assets/icons/icon-poll.svg | 1 + .../Umbraco/assets/icons/icon-post-it.svg | 1 + .../Umbraco/assets/icons/icon-pound-bag.svg | 1 + .../assets/icons/icon-power-outlet.svg | 1 + TestSite/Umbraco/assets/icons/icon-power.svg | 1 + .../assets/icons/icon-presentation.svg | 1 + .../assets/icons/icon-previous-media.svg | 1 + .../Umbraco/assets/icons/icon-previous.svg | 1 + .../assets/icons/icon-price-dollar.svg | 1 + .../Umbraco/assets/icons/icon-price-euro.svg | 1 + .../Umbraco/assets/icons/icon-price-pound.svg | 1 + .../Umbraco/assets/icons/icon-price-yen.svg | 1 + TestSite/Umbraco/assets/icons/icon-print.svg | 1 + .../Umbraco/assets/icons/icon-printer-alt.svg | 1 + .../Umbraco/assets/icons/icon-projector.svg | 1 + TestSite/Umbraco/assets/icons/icon-pulse.svg | 1 + .../Umbraco/assets/icons/icon-pushpin.svg | 1 + .../Umbraco/assets/icons/icon-qr-code.svg | 1 + TestSite/Umbraco/assets/icons/icon-quote.svg | 1 + .../Umbraco/assets/icons/icon-radio-alt.svg | 1 + .../assets/icons/icon-radio-receiver.svg | 1 + TestSite/Umbraco/assets/icons/icon-radio.svg | 1 + TestSite/Umbraco/assets/icons/icon-rain.svg | 1 + TestSite/Umbraco/assets/icons/icon-rate.svg | 1 + .../Umbraco/assets/icons/icon-re-post.svg | 1 + .../Umbraco/assets/icons/icon-readonly.svg | 1 + .../Umbraco/assets/icons/icon-receipt-alt.svg | 1 + .../assets/icons/icon-receipt-dollar.svg | 1 + .../assets/icons/icon-receipt-euro.svg | 1 + .../assets/icons/icon-receipt-pound.svg | 1 + .../Umbraco/assets/icons/icon-receipt-yen.svg | 1 + .../Umbraco/assets/icons/icon-reception.svg | 1 + TestSite/Umbraco/assets/icons/icon-record.svg | 1 + TestSite/Umbraco/assets/icons/icon-redo.svg | 1 + .../Umbraco/assets/icons/icon-refresh.svg | 1 + TestSite/Umbraco/assets/icons/icon-remote.svg | 1 + TestSite/Umbraco/assets/icons/icon-remove.svg | 1 + .../Umbraco/assets/icons/icon-repeat-one.svg | 1 + TestSite/Umbraco/assets/icons/icon-repeat.svg | 1 + .../Umbraco/assets/icons/icon-reply-arrow.svg | 1 + TestSite/Umbraco/assets/icons/icon-resize.svg | 1 + .../assets/icons/icon-return-to-top.svg | 1 + .../assets/icons/icon-right-double-arrow.svg | 1 + TestSite/Umbraco/assets/icons/icon-road.svg | 1 + .../Umbraco/assets/icons/icon-roadsign.svg | 1 + TestSite/Umbraco/assets/icons/icon-rocket.svg | 1 + TestSite/Umbraco/assets/icons/icon-rss.svg | 1 + .../Umbraco/assets/icons/icon-ruler-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-ruler.svg | 1 + TestSite/Umbraco/assets/icons/icon-safe.svg | 1 + .../Umbraco/assets/icons/icon-safedial.svg | 1 + .../assets/icons/icon-sandbox-toys.svg | 1 + .../assets/icons/icon-satellite-dish.svg | 1 + TestSite/Umbraco/assets/icons/icon-save.svg | 1 + TestSite/Umbraco/assets/icons/icon-scan.svg | 1 + TestSite/Umbraco/assets/icons/icon-school.svg | 1 + .../assets/icons/icon-screensharing.svg | 1 + .../Umbraco/assets/icons/icon-script-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-script.svg | 1 + TestSite/Umbraco/assets/icons/icon-scull.svg | 1 + TestSite/Umbraco/assets/icons/icon-search.svg | 1 + .../assets/icons/icon-security-camera.svg | 1 + TestSite/Umbraco/assets/icons/icon-sensor.svg | 1 + .../Umbraco/assets/icons/icon-server-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-server.svg | 1 + .../assets/icons/icon-settings-alt-2.svg | 1 + .../assets/icons/icon-settings-alt.svg | 1 + .../Umbraco/assets/icons/icon-settings.svg | 1 + .../Umbraco/assets/icons/icon-share-alt-2.svg | 1 + .../Umbraco/assets/icons/icon-share-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-share.svg | 1 + .../assets/icons/icon-sharing-iphone.svg | 1 + TestSite/Umbraco/assets/icons/icon-shield.svg | 1 + TestSite/Umbraco/assets/icons/icon-shift.svg | 1 + .../assets/icons/icon-shipping-box.svg | 1 + .../Umbraco/assets/icons/icon-shipping.svg | 1 + TestSite/Umbraco/assets/icons/icon-shoe.svg | 1 + .../icons/icon-shopping-basket-alt-2.svg | 1 + .../assets/icons/icon-shopping-basket-alt.svg | 1 + .../assets/icons/icon-shopping-basket.svg | 1 + TestSite/Umbraco/assets/icons/icon-shorts.svg | 1 + .../Umbraco/assets/icons/icon-shuffle.svg | 1 + TestSite/Umbraco/assets/icons/icon-sience.svg | 1 + .../Umbraco/assets/icons/icon-simcard.svg | 1 + .../Umbraco/assets/icons/icon-single-note.svg | 1 + .../Umbraco/assets/icons/icon-sitemap.svg | 1 + TestSite/Umbraco/assets/icons/icon-sleep.svg | 1 + .../Umbraco/assets/icons/icon-slideshow.svg | 1 + .../assets/icons/icon-smiley-inverted.svg | 1 + TestSite/Umbraco/assets/icons/icon-smiley.svg | 1 + TestSite/Umbraco/assets/icons/icon-snow.svg | 1 + .../Umbraco/assets/icons/icon-sound-low.svg | 1 + .../assets/icons/icon-sound-medium.svg | 1 + .../Umbraco/assets/icons/icon-sound-off.svg | 1 + .../Umbraco/assets/icons/icon-sound-waves.svg | 1 + TestSite/Umbraco/assets/icons/icon-sound.svg | 1 + TestSite/Umbraco/assets/icons/icon-spades.svg | 1 + .../Umbraco/assets/icons/icon-speaker.svg | 1 + .../Umbraco/assets/icons/icon-speed-gauge.svg | 1 + .../Umbraco/assets/icons/icon-split-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-split.svg | 1 + TestSite/Umbraco/assets/icons/icon-sprout.svg | 1 + .../assets/icons/icon-squiggly-line.svg | 1 + TestSite/Umbraco/assets/icons/icon-ssd.svg | 1 + .../assets/icons/icon-stacked-disks.svg | 1 + TestSite/Umbraco/assets/icons/icon-stamp.svg | 1 + .../Umbraco/assets/icons/icon-stop-alt.svg | 1 + .../Umbraco/assets/icons/icon-stop-hand.svg | 1 + TestSite/Umbraco/assets/icons/icon-stop.svg | 1 + TestSite/Umbraco/assets/icons/icon-store.svg | 1 + TestSite/Umbraco/assets/icons/icon-stream.svg | 1 + TestSite/Umbraco/assets/icons/icon-sunny.svg | 1 + .../Umbraco/assets/icons/icon-sweatshirt.svg | 1 + TestSite/Umbraco/assets/icons/icon-sync.svg | 1 + .../Umbraco/assets/icons/icon-t-shirt.svg | 1 + .../Umbraco/assets/icons/icon-tab-key.svg | 1 + TestSite/Umbraco/assets/icons/icon-tab.svg | 1 + .../Umbraco/assets/icons/icon-tactics.svg | 1 + TestSite/Umbraco/assets/icons/icon-tag.svg | 1 + TestSite/Umbraco/assets/icons/icon-tags.svg | 1 + .../assets/icons/icon-takeaway-cup.svg | 1 + TestSite/Umbraco/assets/icons/icon-target.svg | 1 + .../assets/icons/icon-temperatrure-alt.svg | 1 + .../Umbraco/assets/icons/icon-temperature.svg | 1 + .../Umbraco/assets/icons/icon-terminal.svg | 1 + .../Umbraco/assets/icons/icon-theater.svg | 1 + TestSite/Umbraco/assets/icons/icon-theif.svg | 1 + .../assets/icons/icon-thought-bubble.svg | 1 + .../Umbraco/assets/icons/icon-thumb-down.svg | 1 + .../Umbraco/assets/icons/icon-thumb-up.svg | 1 + .../assets/icons/icon-thumbnail-list.svg | 1 + .../assets/icons/icon-thumbnails-small.svg | 1 + .../Umbraco/assets/icons/icon-thumbnails.svg | 1 + TestSite/Umbraco/assets/icons/icon-ticket.svg | 1 + TestSite/Umbraco/assets/icons/icon-time.svg | 1 + TestSite/Umbraco/assets/icons/icon-timer.svg | 1 + TestSite/Umbraco/assets/icons/icon-tools.svg | 1 + TestSite/Umbraco/assets/icons/icon-top.svg | 1 + .../Umbraco/assets/icons/icon-traffic-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-trafic.svg | 1 + TestSite/Umbraco/assets/icons/icon-train.svg | 1 + .../Umbraco/assets/icons/icon-trash-alt-2.svg | 1 + .../Umbraco/assets/icons/icon-trash-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-trash.svg | 1 + TestSite/Umbraco/assets/icons/icon-tree.svg | 1 + TestSite/Umbraco/assets/icons/icon-trophy.svg | 1 + TestSite/Umbraco/assets/icons/icon-truck.svg | 1 + TestSite/Umbraco/assets/icons/icon-tv-old.svg | 1 + TestSite/Umbraco/assets/icons/icon-tv.svg | 1 + .../Umbraco/assets/icons/icon-umb-content.svg | 1 + .../Umbraco/assets/icons/icon-umb-contour.svg | 1 + .../Umbraco/assets/icons/icon-umb-deploy.svg | 1 + .../assets/icons/icon-umb-developer.svg | 1 + .../Umbraco/assets/icons/icon-umb-media.svg | 1 + .../Umbraco/assets/icons/icon-umb-members.svg | 1 + .../assets/icons/icon-umb-settings.svg | 1 + .../Umbraco/assets/icons/icon-umb-users.svg | 1 + .../Umbraco/assets/icons/icon-umbraco.svg | 1 + .../Umbraco/assets/icons/icon-umbrella.svg | 1 + TestSite/Umbraco/assets/icons/icon-undo.svg | 1 + .../Umbraco/assets/icons/icon-universal.svg | 1 + .../Umbraco/assets/icons/icon-unlocked.svg | 1 + .../Umbraco/assets/icons/icon-untitled.svg | 1 + .../assets/icons/icon-usb-connector.svg | 1 + TestSite/Umbraco/assets/icons/icon-usb.svg | 1 + .../Umbraco/assets/icons/icon-user-female.svg | 1 + .../assets/icons/icon-user-females-alt.svg | 1 + .../assets/icons/icon-user-females.svg | 1 + .../assets/icons/icon-user-glasses.svg | 1 + TestSite/Umbraco/assets/icons/icon-user.svg | 1 + .../Umbraco/assets/icons/icon-users-alt.svg | 1 + TestSite/Umbraco/assets/icons/icon-users.svg | 1 + .../Umbraco/assets/icons/icon-utilities.svg | 1 + TestSite/Umbraco/assets/icons/icon-vcard.svg | 1 + TestSite/Umbraco/assets/icons/icon-video.svg | 1 + TestSite/Umbraco/assets/icons/icon-voice.svg | 1 + .../Umbraco/assets/icons/icon-wall-plug.svg | 1 + TestSite/Umbraco/assets/icons/icon-wallet.svg | 1 + TestSite/Umbraco/assets/icons/icon-wand.svg | 1 + TestSite/Umbraco/assets/icons/icon-war.svg | 1 + TestSite/Umbraco/assets/icons/icon-weight.svg | 1 + TestSite/Umbraco/assets/icons/icon-width.svg | 1 + TestSite/Umbraco/assets/icons/icon-wifi.svg | 1 + .../assets/icons/icon-window-popin.svg | 1 + .../assets/icons/icon-window-sizes.svg | 1 + .../Umbraco/assets/icons/icon-windows.svg | 1 + .../Umbraco/assets/icons/icon-wine-glass.svg | 1 + TestSite/Umbraco/assets/icons/icon-wrench.svg | 1 + TestSite/Umbraco/assets/icons/icon-wrong.svg | 1 + .../Umbraco/assets/icons/icon-yen-bag.svg | 1 + TestSite/Umbraco/assets/icons/icon-zip.svg | 1 + .../Umbraco/assets/icons/icon-zom-out.svg | 1 + .../Umbraco/assets/icons/icon-zoom-in.svg | 1 + .../src-min-noconflict/mode-json.js | 8 + .../ace-builds/src-min-noconflict/mode-xml.js | 8 + .../src-min-noconflict/snippets/json.js | 8 + .../src-min-noconflict/snippets/xml.js | 8 + .../src-min-noconflict/worker-json.js | 1 + .../src-min-noconflict/worker-xml.js | 1 + .../lib/angular-aria/angular-aria.min.js.map | 8 + TestSite/Umbraco/lib/angular/angular.js | 481 +- .../Umbraco/lib/bootstrap/less/pager.less | 24 +- .../lib/bootstrap/less/pagination.less | 31 +- TestSite/Umbraco/lib/bootstrap/less/type.less | 28 +- TestSite/Umbraco/lib/chart.js/chart.min.js | 6 +- TestSite/Umbraco/lib/flatpickr/flatpickr.css | 84 +- TestSite/Umbraco/lib/flatpickr/flatpickr.js | 4631 ++++++----- TestSite/Umbraco/lib/flatpickr/l10n/ar.js | 61 +- TestSite/Umbraco/lib/flatpickr/l10n/at.js | 73 +- TestSite/Umbraco/lib/flatpickr/l10n/az.js | 74 + TestSite/Umbraco/lib/flatpickr/l10n/be.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/bg.js | 75 +- TestSite/Umbraco/lib/flatpickr/l10n/bn.js | 73 +- TestSite/Umbraco/lib/flatpickr/l10n/bs.js | 66 + TestSite/Umbraco/lib/flatpickr/l10n/cat.js | 110 +- TestSite/Umbraco/lib/flatpickr/l10n/cs.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/cy.js | 109 +- TestSite/Umbraco/lib/flatpickr/l10n/da.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/de.js | 74 +- .../Umbraco/lib/flatpickr/l10n/default.js | 96 +- TestSite/Umbraco/lib/flatpickr/l10n/eo.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/es.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/et.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/fa.js | 77 +- TestSite/Umbraco/lib/flatpickr/l10n/fi.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/fo.js | 74 + TestSite/Umbraco/lib/flatpickr/l10n/fr.js | 81 +- TestSite/Umbraco/lib/flatpickr/l10n/ga.js | 66 + TestSite/Umbraco/lib/flatpickr/l10n/gr.js | 77 +- TestSite/Umbraco/lib/flatpickr/l10n/he.js | 67 +- TestSite/Umbraco/lib/flatpickr/l10n/hi.js | 73 +- TestSite/Umbraco/lib/flatpickr/l10n/hr.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/hu.js | 79 +- TestSite/Umbraco/lib/flatpickr/l10n/id.js | 71 +- TestSite/Umbraco/lib/flatpickr/l10n/index.js | 4534 +++++++--- TestSite/Umbraco/lib/flatpickr/l10n/is.js | 72 + TestSite/Umbraco/lib/flatpickr/l10n/it.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/ja.js | 80 +- TestSite/Umbraco/lib/flatpickr/l10n/ka.js | 75 + TestSite/Umbraco/lib/flatpickr/l10n/km.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/ko.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/kz.js | 77 +- TestSite/Umbraco/lib/flatpickr/l10n/lt.js | 79 +- TestSite/Umbraco/lib/flatpickr/l10n/lv.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/mk.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/mn.js | 66 +- TestSite/Umbraco/lib/flatpickr/l10n/ms.js | 75 +- TestSite/Umbraco/lib/flatpickr/l10n/my.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/nl.js | 83 +- TestSite/Umbraco/lib/flatpickr/l10n/no.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/pa.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/pl.js | 82 +- TestSite/Umbraco/lib/flatpickr/l10n/pt.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/ro.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/ru.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/si.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/sk.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/sl.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/sq.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/sr-cyr.js | 67 + TestSite/Umbraco/lib/flatpickr/l10n/sr.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/sv.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/th.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/tr.js | 78 +- TestSite/Umbraco/lib/flatpickr/l10n/uk.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/uz.js | 75 + .../Umbraco/lib/flatpickr/l10n/uz_latn.js | 75 + TestSite/Umbraco/lib/flatpickr/l10n/vn.js | 74 +- TestSite/Umbraco/lib/flatpickr/l10n/zh-tw.js | 68 + TestSite/Umbraco/lib/flatpickr/l10n/zh.js | 73 +- TestSite/Umbraco/lib/jquery/jquery.min.js | 4 +- TestSite/Umbraco/lib/jquery/jquery.min.map | 2 +- .../Umbraco/lib/nouislider/nouislider.min.css | 3 +- .../Umbraco/lib/nouislider/nouislider.min.js | 3 +- TestSite/Umbraco/lib/spectrum/spectrum.css | 360 +- TestSite/Umbraco/lib/spectrum/spectrum.js | 759 +- TestSite/Umbraco/lib/tinymce/langs/af_ZA.js | 230 + TestSite/Umbraco/lib/tinymce/langs/ar.js | 262 + TestSite/Umbraco/lib/tinymce/langs/az.js | 261 + TestSite/Umbraco/lib/tinymce/langs/be.js | 261 + TestSite/Umbraco/lib/tinymce/langs/bg_BG.js | 261 + TestSite/Umbraco/lib/tinymce/langs/bn_BD.js | 261 + TestSite/Umbraco/lib/tinymce/langs/ca.js | 261 + TestSite/Umbraco/lib/tinymce/langs/cs.js | 261 + TestSite/Umbraco/lib/tinymce/langs/cs_CZ.js | 260 + TestSite/Umbraco/lib/tinymce/langs/cy.js | 230 + TestSite/Umbraco/lib/tinymce/langs/da.js | 304 +- TestSite/Umbraco/lib/tinymce/langs/de.js | 304 +- TestSite/Umbraco/lib/tinymce/langs/de_AT.js | 261 + TestSite/Umbraco/lib/tinymce/langs/dv.js | 230 + TestSite/Umbraco/lib/tinymce/langs/el.js | 261 + TestSite/Umbraco/lib/tinymce/langs/en_CA.js | 261 + TestSite/Umbraco/lib/tinymce/langs/en_GB.js | 261 + TestSite/Umbraco/lib/tinymce/langs/en_us.js | 262 +- TestSite/Umbraco/lib/tinymce/langs/es.js | 261 + TestSite/Umbraco/lib/tinymce/langs/es_MX.js | 261 + TestSite/Umbraco/lib/tinymce/langs/et.js | 261 + TestSite/Umbraco/lib/tinymce/langs/eu.js | 261 + TestSite/Umbraco/lib/tinymce/langs/fa_IR.js | 262 + TestSite/Umbraco/lib/tinymce/langs/fi.js | 302 +- TestSite/Umbraco/lib/tinymce/langs/fr.js | 390 +- TestSite/Umbraco/lib/tinymce/langs/fr_FR.js | 261 + TestSite/Umbraco/lib/tinymce/langs/ga.js | 261 + TestSite/Umbraco/lib/tinymce/langs/gl.js | 253 + TestSite/Umbraco/lib/tinymce/langs/he_IL.js | 262 + TestSite/Umbraco/lib/tinymce/langs/hr.js | 253 + TestSite/Umbraco/lib/tinymce/langs/hu_HU.js | 261 + TestSite/Umbraco/lib/tinymce/langs/id.js | 261 + TestSite/Umbraco/lib/tinymce/langs/it.js | 304 +- TestSite/Umbraco/lib/tinymce/langs/ja.js | 302 +- TestSite/Umbraco/lib/tinymce/langs/ka_GE.js | 230 + TestSite/Umbraco/lib/tinymce/langs/kab.js | 261 + TestSite/Umbraco/lib/tinymce/langs/kk.js | 230 + TestSite/Umbraco/lib/tinymce/langs/km_KH.js | 253 + TestSite/Umbraco/lib/tinymce/langs/ko_KR.js | 261 + TestSite/Umbraco/lib/tinymce/langs/lt.js | 261 + TestSite/Umbraco/lib/tinymce/langs/lv.js | 260 + TestSite/Umbraco/lib/tinymce/langs/nb_NO.js | 261 + TestSite/Umbraco/lib/tinymce/langs/nl.js | 304 +- TestSite/Umbraco/lib/tinymce/langs/pl.js | 302 +- TestSite/Umbraco/lib/tinymce/langs/pt_BR.js | 261 + TestSite/Umbraco/lib/tinymce/langs/pt_PT.js | 261 + TestSite/Umbraco/lib/tinymce/langs/ro.js | 230 + TestSite/Umbraco/lib/tinymce/langs/ru.js | 302 +- TestSite/Umbraco/lib/tinymce/langs/sk.js | 253 + TestSite/Umbraco/lib/tinymce/langs/sl_SI.js | 230 + TestSite/Umbraco/lib/tinymce/langs/sr.js | 261 + TestSite/Umbraco/lib/tinymce/langs/sv_SE.js | 261 + TestSite/Umbraco/lib/tinymce/langs/ta.js | 261 + TestSite/Umbraco/lib/tinymce/langs/ta_IN.js | 261 + TestSite/Umbraco/lib/tinymce/langs/th_TH.js | 261 + TestSite/Umbraco/lib/tinymce/langs/tr.js | 261 + TestSite/Umbraco/lib/tinymce/langs/tr_TR.js | 261 + TestSite/Umbraco/lib/tinymce/langs/ug.js | 260 + TestSite/Umbraco/lib/tinymce/langs/uk.js | 261 + TestSite/Umbraco/lib/tinymce/langs/uk_UA.js | 261 + TestSite/Umbraco/lib/tinymce/langs/uz.js | 260 + TestSite/Umbraco/lib/tinymce/langs/vi_VN.js | 260 + TestSite/Umbraco/lib/tinymce/langs/zh_CN.js | 261 + TestSite/Umbraco/lib/tinymce/langs/zh_TW.js | 261 + .../lib/tinymce/plugins/media/plugin.js | 492 +- .../lib/tinymce/plugins/media/plugin.min.js | 2 +- .../lib/tinymce/plugins/table/plugin.js | 3 + .../lib/tinymce/plugins/table/plugin.min.js | 2 +- .../skins/lightgray/content.inline.min.css | 2 +- .../tinymce/skins/lightgray/content.min.css | 2 +- .../lib/tinymce/themes/mobile/theme.js | 24 - .../lib/tinymce/themes/mobile/theme.min.js | 2 +- TestSite/Umbraco/lib/tinymce/tinymce.min.js | 4 +- TestSite/Umbraco/lib/umbraco/Extensions.js | 12 + .../Umbraco/lib/underscore/underscore-min.js | 9 +- .../Umbraco/lib/wicg-inert/dist/inert.min.js | 2 + .../lib/wicg-inert/dist/inert.min.js.map | 1 + .../Views/Partials/BlockList/Default.cshtml | 13 + .../Partials/Grid/Bootstrap3-Fluid.cshtml | 16 +- .../Views/Partials/Grid/Bootstrap3.cshtml | 14 +- .../Views/Partials/Grid/Editors/Media.cshtml | 51 +- .../Views/Partials/Grid/Editors/Rte.cshtml | 10 +- .../Partials/Grid/Editors/Textstring.cshtml | 2 +- TestSite/Views/Web.config | 19 +- TestSite/Web.config | 39 +- .../BackOfficeTours/getting-started.json | 34 +- TestSite/config/imageprocessor/cache.config | 6 +- .../config/imageprocessor/processing.config | 10 +- .../config/imageprocessor/security.config | 6 +- TestSite/config/umbracoSettings.config | 54 +- TestSite/packages.config | 47 +- 1122 files changed, 60846 insertions(+), 17709 deletions(-) create mode 100644 TestSite/Umbraco/Config/Lang/cy.xml create mode 100644 TestSite/Umbraco/Js/umbraco.websitepreview.js create mode 100644 TestSite/Umbraco/Js/utilities.js create mode 100644 TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.content.html create mode 100644 TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.html create mode 100644 TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.settings.html create mode 100644 TestSite/Umbraco/Views/common/infiniteeditors/blockpicker/blockpicker.html create mode 100644 TestSite/Umbraco/Views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html create mode 100644 TestSite/Umbraco/Views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html create mode 100644 TestSite/Umbraco/Views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html create mode 100644 TestSite/Umbraco/Views/common/overlays/changepassword/changepassword.html create mode 100644 TestSite/Umbraco/Views/common/tours/umbEmailMarketing/confirm/confirm.html create mode 100644 TestSite/Umbraco/Views/components/blockcard/umb-block-card.html create mode 100644 TestSite/Umbraco/Views/components/buttons/umb-button-ellipsis.html create mode 100644 TestSite/Umbraco/Views/components/contenttype/umb-content-type-group.html create mode 100644 TestSite/Umbraco/Views/components/contenttype/umb-content-type-groups.html create mode 100644 TestSite/Umbraco/Views/components/contenttype/umb-content-type-property.html create mode 100644 TestSite/Umbraco/Views/components/contenttype/umb-content-type-tab.html create mode 100644 TestSite/Umbraco/Views/components/editor/umb-editor-tab-bar.html create mode 100644 TestSite/Umbraco/Views/components/elementeditor/umb-element-editor-content.component.html create mode 100644 TestSite/Umbraco/Views/components/forms/umb-search-filter.html create mode 100644 TestSite/Umbraco/Views/components/mediacard/umb-media-card.html create mode 100644 TestSite/Umbraco/Views/components/property/umb-property-actions.html create mode 100644 TestSite/Umbraco/Views/components/umb-icon.html create mode 100644 TestSite/Umbraco/Views/components/umb-mini-search.html create mode 100644 TestSite/Umbraco/Views/components/umb-property-info-button/umb-property-info-button.html create mode 100644 TestSite/Umbraco/Views/errors/BootFailed.html create mode 100644 TestSite/Umbraco/Views/membertypes/copy.html create mode 100644 TestSite/Umbraco/Views/prevalueeditors/checkboxlist.html create mode 100644 TestSite/Umbraco/Views/prevalueeditors/numberrange.html create mode 100644 TestSite/Umbraco/Views/prevalueeditors/overlaysize.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/blocklist/blocklist.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/blocklist/umb-block-list-property-editor.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/blocklist/umb-block-list-row.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/eyedropper/eyedropper.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/grid/overlays/rowdeleteconfirm.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/mediapicker3/mediapicker3.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/notsupported/notsupported.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/userpicker/overlays/remove.html create mode 100644 TestSite/Umbraco/Views/propertyeditors/userpicker/userpicker.html create mode 100644 TestSite/Umbraco/Views/users/views/overlays/remove.html create mode 100644 TestSite/Umbraco/assets/icons/icon-activity.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-add.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-addressbook.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-alarm-clock.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-alert-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-alert.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-anchor.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-app.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-application-error.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-application-window-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-application-window.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-arrivals.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-arrow-down.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-arrow-left.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-arrow-right.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-arrow-up.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-art-easel.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-article.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-attachment.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-auction-hammer.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-autofill.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-award.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-axis-rotation-2.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-axis-rotation-3.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-axis-rotation.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-baby-stroller.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-backspace.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-badge-add.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-badge-count.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-badge-remove.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-badge-restricted.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-ball.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-band-aid.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bar-chart.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-barcode.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bars.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-battery-full.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-battery-low.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-beer-glass.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bell-off.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bell.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bill-dollar.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bill-euro.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bill-pound.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bill-yen.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bill.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-billboard.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bills-dollar.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bills-euro.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bills-pound.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bills-yen.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bills.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-binarycode.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-binoculars.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bird.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-birthday-cake.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-block.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-blueprint.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bluetooth.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-boat-shipping.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bomb.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bones.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-book-alt-2.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-book-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-book.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bookmark.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-books.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-box-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-box-open.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-box.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-brackets.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-brick.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-briefcase.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-browser-window.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-brush-alt-2.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-brush-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-brush.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bug.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bulleted-list.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-burn.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-bus.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-calculator.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-calendar-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-calendar.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-camcorder.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-camera-roll.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-candy.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-caps-lock.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-car.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-cash-register.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-categories.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-certificate.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-chart-curve.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-chart.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-chat-active.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-chat.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-check.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-checkbox-dotted-active.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-checkbox-dotted.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-checkbox-empty.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-checkbox.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-chess.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-chip-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-chip.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-cinema.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-circle-dotted-active.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-circle-dotted.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-circuits.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-circus.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-client.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-clothes-hanger.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-cloud-drive.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-cloud-upload.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-cloud.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-cloudy.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-clubs.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-cocktail.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-code.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coffee.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coin-dollar.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coin-euro.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coin-pound.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coin-yen.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coin.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins-dollar-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins-dollar.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins-euro-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins-euro.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins-pound-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins-pound.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins-yen-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins-yen.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coins.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-color-bucket.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-colorpicker.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-columns.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-comb.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-combination-lock-open.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-combination-lock.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-command.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-company.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-compress.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-connection.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-console.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-contrast.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-conversation-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-conversation.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-coverflow.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-credit-card-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-credit-card.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-crop.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-crosshair.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-crown-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-crown.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-cupcake.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-curve.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-cut.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-dashboard.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-defrag.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-delete-key.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-delete.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-departure.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-desk.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-desktop.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-diagnostics.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-diagonal-arrow-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-diagonal-arrow.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-diamond.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-diamonds.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-dice.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-diploma-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-diploma.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-directions-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-directions.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-disc.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-disk-image.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-display.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-dna.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-dock-connector.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-document-dashed-line.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-document.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-documents.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-dollar-bag.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-donate.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-door-open-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-door-open.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-download-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-download.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-drop.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-eco.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-economy.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-edit.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-eject.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-employee.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-energy-saving-bulb.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-enter.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-equalizer.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-escape.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-ethernet.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-euro-bag.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-exit-fullscreen.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-eye.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-facebook-like.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-factory.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-favorite.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-female-symbol.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-file-cabinet.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-files.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-filter-arrows.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-filter.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-fingerprint.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-fire.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-firewall.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-firewire.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-flag-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-flag.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-flash.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-flashlight.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-flowerpot.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-folder-open.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-folder-outline.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-folder.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-folders.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-font.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-food.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-footprints.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-forking.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-frame-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-frame.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-fullscreen-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-fullscreen.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-game.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-geometry.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-gift.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-glasses.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-globe-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-globe-asia.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-globe-europe-africa.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-globe-inverted-america.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-globe-inverted-asia.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-globe-inverted-europe-africa.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-globe.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-gps.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-graduate.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-grid.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hammer.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hand-active-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hand-active.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hand-pointer-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hand-pointer.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-handprint.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-handshake.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-handtool-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-handtool.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hard-drive-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hard-drive.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hat.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hd.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-headphones.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-headset.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hearts.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-height.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-help-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-help.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-home.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-hourglass.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-imac.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-inactive-line.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-inbox-full.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-inbox.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-indent.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-infinity.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-info.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-invoice.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-ipad.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-iphone.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-item-arrangement.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-junk.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-key.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-keyboard.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-keychain.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-keyhole.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-lab.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-laptop.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-layers-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-layers.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-layout.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-left-double-arrow.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-legal.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-lense.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-library.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-light-down.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-light-up.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-lightbulb-active.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-lightbulb.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-lightning.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-link.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-linux-tux.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-list.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-load.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-loading.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-locate.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-location-near-me.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-location-nearby.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-lock.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-log-out.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-logout.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-loupe.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-magnet.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-mailbox.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-male-and-female.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-male-symbol.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-map-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-map-location.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-map-marker.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-map.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-medal.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-medical-emergency.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-medicine.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-meeting.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-megaphone.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-merge.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-message-open.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-message-unopened.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-message.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-microscope.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-mindmap.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-mobile.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-molecular-network.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-molecular.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-mountain.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-mouse-cursor.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-mouse.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-movie-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-movie.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-multiple-credit-cards.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-multiple-windows.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-music.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-name-badge.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-bottom.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-down.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-first.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-horizontal.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-last.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-left.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-right.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-road.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-top.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-up.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation-vertical.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigation.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-navigational-arrow.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-network-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-newspaper-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-newspaper.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-next-media.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-next.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-nodes.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-notepad-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-notepad.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-old-key.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-old-phone.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-operator.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-ordered-list.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-os-x.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-out.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-outbox.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-outdent.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-page-add.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-page-down.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-page-remove.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-page-restricted.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-page-up.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-paint-roller.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-palette.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-panel-show.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pannel-close.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pants.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-paper-bag.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-paper-plane-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-paper-plane.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-parachute-drop.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-parental-control.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-partly-cloudy.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-paste-in.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-path.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pause.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pc.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-people-alt-2.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-people-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-people-female.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-people.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-phone-ring.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-phone.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-photo-album.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-picture.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pictures-alt-2.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pictures-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pictures.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pie-chart.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-piggy-bank.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pin-location.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-piracy.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-plane.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-planet.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-play.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-playing-cards.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-playlist.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-plugin.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-podcast.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-poker-chip.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-poll.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-post-it.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pound-bag.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-power-outlet.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-power.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-presentation.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-previous-media.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-previous.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-price-dollar.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-price-euro.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-price-pound.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-price-yen.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-print.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-printer-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-projector.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pulse.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-pushpin.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-qr-code.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-quote.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-radio-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-radio-receiver.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-radio.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-rain.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-rate.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-re-post.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-readonly.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-receipt-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-receipt-dollar.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-receipt-euro.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-receipt-pound.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-receipt-yen.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-reception.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-record.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-redo.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-refresh.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-remote.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-remove.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-repeat-one.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-repeat.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-reply-arrow.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-resize.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-return-to-top.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-right-double-arrow.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-road.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-roadsign.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-rocket.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-rss.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-ruler-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-ruler.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-safe.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-safedial.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sandbox-toys.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-satellite-dish.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-save.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-scan.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-school.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-screensharing.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-script-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-script.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-scull.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-search.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-security-camera.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sensor.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-server-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-server.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-settings-alt-2.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-settings-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-settings.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-share-alt-2.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-share-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-share.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sharing-iphone.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shield.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shift.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shipping-box.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shipping.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shoe.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shopping-basket-alt-2.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shopping-basket-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shopping-basket.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shorts.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-shuffle.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sience.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-simcard.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-single-note.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sitemap.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sleep.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-slideshow.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-smiley-inverted.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-smiley.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-snow.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sound-low.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sound-medium.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sound-off.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sound-waves.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sound.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-spades.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-speaker.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-speed-gauge.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-split-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-split.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sprout.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-squiggly-line.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-ssd.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-stacked-disks.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-stamp.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-stop-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-stop-hand.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-stop.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-store.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-stream.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sunny.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sweatshirt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-sync.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-t-shirt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-tab-key.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-tab.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-tactics.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-tag.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-tags.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-takeaway-cup.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-target.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-temperatrure-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-temperature.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-terminal.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-theater.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-theif.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-thought-bubble.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-thumb-down.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-thumb-up.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-thumbnail-list.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-thumbnails-small.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-thumbnails.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-ticket.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-time.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-timer.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-tools.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-top.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-traffic-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-trafic.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-train.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-trash-alt-2.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-trash-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-trash.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-tree.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-trophy.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-truck.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-tv-old.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-tv.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umb-content.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umb-contour.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umb-deploy.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umb-developer.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umb-media.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umb-members.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umb-settings.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umb-users.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umbraco.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-umbrella.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-undo.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-universal.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-unlocked.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-untitled.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-usb-connector.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-usb.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-user-female.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-user-females-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-user-females.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-user-glasses.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-user.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-users-alt.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-users.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-utilities.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-vcard.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-video.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-voice.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-wall-plug.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-wallet.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-wand.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-war.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-weight.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-width.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-wifi.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-window-popin.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-window-sizes.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-windows.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-wine-glass.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-wrench.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-wrong.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-yen-bag.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-zip.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-zom-out.svg create mode 100644 TestSite/Umbraco/assets/icons/icon-zoom-in.svg create mode 100644 TestSite/Umbraco/lib/ace-builds/src-min-noconflict/mode-json.js create mode 100644 TestSite/Umbraco/lib/ace-builds/src-min-noconflict/mode-xml.js create mode 100644 TestSite/Umbraco/lib/ace-builds/src-min-noconflict/snippets/json.js create mode 100644 TestSite/Umbraco/lib/ace-builds/src-min-noconflict/snippets/xml.js create mode 100644 TestSite/Umbraco/lib/ace-builds/src-min-noconflict/worker-json.js create mode 100644 TestSite/Umbraco/lib/ace-builds/src-min-noconflict/worker-xml.js create mode 100644 TestSite/Umbraco/lib/angular-aria/angular-aria.min.js.map create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/az.js create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/bs.js create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/fo.js create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/ga.js create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/is.js create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/ka.js create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/sr-cyr.js create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/uz.js create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/uz_latn.js create mode 100644 TestSite/Umbraco/lib/flatpickr/l10n/zh-tw.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/af_ZA.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/ar.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/az.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/be.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/bg_BG.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/bn_BD.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/ca.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/cs.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/cs_CZ.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/cy.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/de_AT.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/dv.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/el.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/en_CA.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/en_GB.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/es.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/es_MX.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/et.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/eu.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/fa_IR.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/fr_FR.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/ga.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/gl.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/he_IL.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/hr.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/hu_HU.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/id.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/ka_GE.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/kab.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/kk.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/km_KH.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/ko_KR.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/lt.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/lv.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/nb_NO.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/pt_BR.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/pt_PT.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/ro.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/sk.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/sl_SI.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/sr.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/sv_SE.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/ta.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/ta_IN.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/th_TH.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/tr.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/tr_TR.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/ug.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/uk.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/uk_UA.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/uz.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/vi_VN.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/zh_CN.js create mode 100644 TestSite/Umbraco/lib/tinymce/langs/zh_TW.js create mode 100644 TestSite/Umbraco/lib/wicg-inert/dist/inert.min.js create mode 100644 TestSite/Umbraco/lib/wicg-inert/dist/inert.min.js.map create mode 100644 TestSite/Views/Partials/BlockList/Default.cshtml diff --git a/TestSite/TestSite.csproj b/TestSite/TestSite.csproj index cf84ae4..99eb147 100644 --- a/TestSite/TestSite.csproj +++ b/TestSite/TestSite.csproj @@ -1,6 +1,5 @@  - - + @@ -57,8 +56,8 @@ ..\packages\EPPlus.4.5.3.2\lib\net40\EPPlus.dll - - ..\packages\Examine.1.0.2\lib\net452\Examine.dll + + ..\packages\Examine.1.2.0\lib\net452\Examine.dll ..\packages\HtmlAgilityPack.1.8.14\lib\Net45\HtmlAgilityPack.dll @@ -66,11 +65,14 @@ ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - ..\packages\ImageProcessor.2.7.0.100\lib\net452\ImageProcessor.dll + + ..\packages\ImageProcessor.2.9.1\lib\net452\ImageProcessor.dll - - ..\packages\ImageProcessor.Web.4.10.0.100\lib\net452\ImageProcessor.Web.dll + + ..\packages\ImageProcessor.Web.4.12.1\lib\net452\ImageProcessor.Web.dll + + + ..\packages\K4os.Compression.LZ4.1.1.11\lib\net46\K4os.Compression.LZ4.dll ..\packages\LightInject.5.4.0\lib\net46\LightInject.dll @@ -93,6 +95,12 @@ ..\packages\Markdown.2.2.1\lib\net451\Markdown.dll + + ..\packages\MessagePack.2.2.85\lib\netstandard2.0\MessagePack.dll + + + ..\packages\MessagePack.Annotations.2.2.85\lib\netstandard2.0\MessagePack.Annotations.dll + ..\packages\Microsoft.AspNet.Identity.Core.2.2.2\lib\net45\Microsoft.AspNet.Identity.Core.dll @@ -102,6 +110,9 @@ ..\packages\Microsoft.AspNet.SignalR.Core.2.4.0\lib\net45\Microsoft.AspNet.SignalR.Core.dll + + ..\packages\Microsoft.Bcl.AsyncInterfaces.1.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll @@ -147,37 +158,43 @@ ..\packages\Semver.2.0.4\lib\net452\Semver.dll - ..\packages\Serilog.2.8.0\lib\net46\Serilog.dll + ..\packages\Serilog.2.10.0\lib\net46\Serilog.dll - ..\packages\Serilog.Enrichers.Process.2.0.1\lib\net45\Serilog.Enrichers.Process.dll + ..\packages\Serilog.Enrichers.Process.2.0.2\lib\net45\Serilog.Enrichers.Process.dll - ..\packages\Serilog.Enrichers.Thread.3.0.0\lib\net45\Serilog.Enrichers.Thread.dll + ..\packages\Serilog.Enrichers.Thread.3.1.0\lib\net45\Serilog.Enrichers.Thread.dll - - ..\packages\Serilog.Filters.Expressions.2.0.0\lib\net45\Serilog.Filters.Expressions.dll + + ..\packages\Serilog.Filters.Expressions.2.1.0\lib\net45\Serilog.Filters.Expressions.dll - - ..\packages\Serilog.Formatting.Compact.1.0.0\lib\net45\Serilog.Formatting.Compact.dll + + ..\packages\Serilog.Formatting.Compact.1.1.0\lib\net452\Serilog.Formatting.Compact.dll - - ..\packages\Serilog.Formatting.Compact.Reader.1.0.3\lib\net45\Serilog.Formatting.Compact.Reader.dll + + ..\packages\Serilog.Formatting.Compact.Reader.1.0.5\lib\net45\Serilog.Formatting.Compact.Reader.dll ..\packages\Serilog.Settings.AppSettings.2.2.2\lib\net45\Serilog.Settings.AppSettings.dll - - ..\packages\Serilog.Sinks.Async.1.3.0\lib\net45\Serilog.Sinks.Async.dll + + ..\packages\Serilog.Sinks.Async.1.5.0\lib\net461\Serilog.Sinks.Async.dll - ..\packages\Serilog.Sinks.File.4.0.0\lib\net45\Serilog.Sinks.File.dll + ..\packages\Serilog.Sinks.File.4.1.0\lib\net45\Serilog.Sinks.File.dll - - ..\packages\Serilog.Sinks.Map.1.0.0\lib\netstandard2.0\Serilog.Sinks.Map.dll + + ..\packages\Serilog.Sinks.Map.1.0.2\lib\netstandard2.0\Serilog.Sinks.Map.dll - ..\packages\Superpower.2.0.0\lib\net45\Superpower.dll + ..\packages\Superpower.2.3.0\lib\net45\Superpower.dll + + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + ..\packages\System.Collections.Immutable.1.7.1\lib\net461\System.Collections.Immutable.dll @@ -191,18 +208,34 @@ + + ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.7.1\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Text.Encoding.CodePages.4.7.1\lib\net461\System.Text.Encoding.CodePages.dll + ..\packages\System.Threading.Tasks.Dataflow.4.9.0\lib\netstandard2.0\System.Threading.Tasks.Dataflow.dll + + ..\packages\System.Threading.Tasks.Extensions.4.5.3\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll + ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll @@ -248,10 +281,10 @@ - ..\packages\UmbracoCms.Core.8.6.3\lib\net472\Umbraco.Core.dll + ..\packages\UmbracoCms.Core.8.17.2\lib\net472\Umbraco.Core.dll - ..\packages\UmbracoCms.Web.8.6.3\lib\net472\Umbraco.Examine.dll + ..\packages\UmbracoCms.Web.8.17.2\lib\net472\Umbraco.Examine.dll ..\packages\UmbracoForms.Core.8.4.1\lib\net472\Umbraco.Forms.Core.dll @@ -266,13 +299,13 @@ ..\packages\UmbracoForms.Core.8.4.1\lib\net472\Umbraco.Licensing.dll - ..\packages\UmbracoCms.Web.8.6.3\lib\net472\Umbraco.ModelsBuilder.Embedded.dll + ..\packages\UmbracoCms.Web.8.17.2\lib\net472\Umbraco.ModelsBuilder.Embedded.dll - ..\packages\UmbracoCms.Web.8.6.3\lib\net472\Umbraco.Web.dll + ..\packages\UmbracoCms.Web.8.17.2\lib\net472\Umbraco.Web.dll - ..\packages\UmbracoCms.Web.8.6.3\lib\net472\Umbraco.Web.UI.dll + ..\packages\UmbracoCms.Web.8.17.2\lib\net472\Umbraco.Web.UI.dll ..\packages\uSync.BackOffice.Core.8.7.1\lib\net472\uSync8.BackOffice.dll @@ -507,14 +540,6 @@ - - - - - - - - @@ -529,19 +554,16 @@ - - - + + + + + + + + - - - - - - - - @@ -583,6 +605,15 @@ + + + + + + + + + Web.config @@ -628,14 +659,11 @@ - - - - + + - - + - Typ - Pro hledání pište... + Stav + Potvrdit + Zadejte + Pište pro vyhledávání... + pod Nahoru Aktualizovat Povýšit Nahrání - Url + URL Uživatel Uživatelské jméno Hodnota @@ -397,18 +735,56 @@ Ano Složka Výsledky hledání - Reorder - I am done reordering + Přesunout + Skončil jsem s přesouváním + Náhled + Změnit heslo + na + Seznam + Ukládám... + aktuální + Vložené + vybrané + Další + Články + Videa + Instalování + + + Modrá + + + Přidat skupinu + Přidat vlastnost + Přidat editor + Přidat šablonu + Přidat vnořený uzel + Přidat potomka + Upravit datový typ + Navigace v sekcích + Klávesové zkratky + zobrazit klávesové zkratky + Přepnout zobrazení seznamu + Přepnout povolení jako root + Okomentovat/Odkomentovat řádky + Odebrat řádek + Kopírovat řádky nahoru + Kopírovat řádky dolů + Přesunout řádky nahoru + Přesunout řádky dolů + Obecný + Editor + Přepnout povolení jazykových verzí - Background color - Bold - Text color + Barva pozadí + Tučně + Barva písma Font Text - Page + Stránka Instalátor se nemůže připojit k databázi. @@ -422,8 +798,8 @@ Databáze nenalezena! Zkontrolujte, prosím, že informace v "připojovacím řetězci" souboru "web.config" jsou správné.

Pro pokračování otevřete, prosím, soubor "web.config" (za pužití Visual Studia nebo Vašeho oblíbeného tedtového editoru), přejděte na jeho konec, přidejte připojovací řetězec pro Vaši databázi v klíčí nazvaném "umbracoDbDSN" a soubor uložte.

- Klikněte na tlačítko zopakovat, až budete hotovi.
- Další informace o editování souboru web.config zde.

]]>
+ Klikněte na tlačítko zopakovat, až budete hotovi.
+ Další informace o editování souboru web.config zde.

]]> Pokud je to nezbytné, kontaktujte vašeho poskytovatele hostingu. Jestliže instalujete na místní počítač nebo server, budete potřebovat informace od Vašeho systémového administrátora.]]> @@ -441,35 +817,35 @@ Heslo výchozího uživatele bylo úspěšně změněno od doby instalace!

Netřeba nic dalšího dělat. Klikněte na Následující pro pokračování.]]> Heslo je změněno! Mějte skvělý start, sledujte naše uváděcí videa - Kliknutím na tlačítko následující (nebo modifikováním umbracoConfigurationStatus v souboru web.config) přijímáte licenci tohoto software tak, jak je uvedena v poli níže. Upozorňujeme, že tato distribuce umbraca se skládá ze dvou různých licencí, open source MIT licence pro framework a umbraco freeware licence, která pokrývá UI. + Kliknutím na tlačítko následující (nebo modifikováním umbracoConfigurationStatus v souboru web.config) přijímáte licenci tohoto software tak, jak je uvedena v poli níže. Upozorňujeme, že tato distribuce Umbraca se skládá ze dvou různých licencí, open source MIT licence pro framework a Umbraco freeware licence, která pokrývá UI. Není nainstalováno. Dotčené soubory a složky - Další informace o nastavování oprávnění pro umbraco zde + Další informace o nastavování oprávnění pro Umbraco zde Musíte udělit ASP.NET oprávnění měnit následující soubory/složky Vaše nastavení oprávnění je téměř dokonalé!

- Můžete provozovat umbraco bez potíží, ale nebudete smět instalovat balíčky, které jsou doporučené pro plné využívání všech možností umbraca.]]>
+ Můžete provozovat Umbraco bez potíží, ale nebudete smět instalovat balíčky, které jsou doporučené pro plné využívání všech možností umbraca.]]> Jak to vyřešit Klikněte zde, chcete-li číst textovou verzi výukové video o nastavovaní oprávnění pro složky umbraca, nebo si přečtěte textovou verzi.]]> Vaše nastavení oprávnění může být problém!

- Můžete provozovat umbraco bez potíží, ale nebudete smět vytvářet složky a instalovat balíčky, které jsou doporučené pro plné využívání všech možností umbraca.]]>
+ Můžete provozovat Umbraco bez potíží, ale nebudete smět vytvářet složky a instalovat balíčky, které jsou doporučené pro plné využívání všech možností umbraca.]]> Vaše nastavení oprívnění není připraveno pro umbraco!

- Abyste mohli umbraco provozovat, budete muset aktualizovat Vaše nastavení oprávnění.]]>
+ Abyste mohli Umbraco provozovat, budete muset aktualizovat Vaše nastavení oprávnění.]]> Vaše nastavení oprávnění je dokonalé!

- Jste připraveni provozovat umbraco a instalovat balíčky!]]>
+ Jste připraveni provozovat Umbraco a instalovat balíčky!]]> Řešení potíží se složkami Následujte tento odkaz pro další informace o potížích s ASP.NET a vytvářením složek. Nastavování oprávnění pro složky Chci začít od nuly zjistěte jak) + (zjistěte jak) Stále se můžete později rozhodnout nainstalovat Runway. Za tím účelem navštivte Vývojářskou sekci a zvolte Balíčky. ]]> Právě jste vytvořili čistou platformu Umbraco. Co chcete dělat dále? @@ -502,7 +878,7 @@ Abyste získali pomoc od naší oceňované komunity, projděte si dokumentaci, nebo si pusťte některá videa zdarma o tom, jak vytvořit jednoduchý web, jak používat balíčky a rychlý úvod do terminologie umbraca]]> Umbraco %0% je nainstalováno a připraveno k použití soubor /web.config a upravit klíč AppSetting umbracoConfigurationStatus dole na hodnotu '%0%'.]]> - ihned začít kliknutím na tlačítko "Spustit Umbraco" níže.
Jestliže je pro Vás umbraco nové, + ihned začít kliknutím na tlačítko "Spustit Umbraco" níže.
Jestliže je pro Vás Umbraco nové, spoustu zdrojů naleznete na naších stránkách "začínáme".]]>
Spustit Umbraco Chcete-li spravovat Váš web, jednoduše přejděte do administrace umbraca a začněte přidávat obsah, upravovat šablony a stylopisy, nebo přidávat nové funkce]]> @@ -515,8 +891,8 @@ Stiskněte "následující" pro spuštění průvodce.]]>
- Kód kultury - Název kultury + Kód jazyka + Název jazyka Byli jste nečinní a odhlášení proběhne automaticky za @@ -531,8 +907,100 @@ Šťastný bláznivý pátek Šťastnou kočkobotu přihlašte se níže + Přihlásit se pomocí Relace vypršela - © 2001 - %0%
umbraco.org

]]>
+ © 2001 - %0%
umbraco.org

]]>
+ Zapomenuté heslo? + Na uvedenou adresu bude zaslán e-mail s odkazem pro obnovení hesla + Pokud odpovídá našim záznamům, bude na zadanou adresu zaslán e-mail s pokyny k obnovení hesla + Zobrazit heslo + Skrýt heslo + Vrátit se na přihlašovací obrazovku + Zadejte nové heslo + Vaše heslo bylo aktualizováno + Odkaz, na který jste klikli, je neplatný nebo jeho platnost vypršela + Umbraco: Resetování hesla + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Vyžadováno resetování hesla +

+

+ Vaše uživatelské jméno pro přihlášení do backoffice Umbraco je: %0% +

+

+ + + + + + +
+ + Kliknutím na tento odkaz obnovíte své heslo + +
+

+

Pokud nemůžete kliknout na odkaz, zkopírujte a vložte tuto adresu URL do okna prohlížeče:

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

+
+
+


+
+
+ + + ]]>
Ovládací panel @@ -555,6 +1023,7 @@ Upravte vaše oznámení pro %0% + Nastavení oznámení bylo uloženo pro - Hi %0%

+ Následující jazyky byly změněny %0% + Ahoj %0%

Toto je automatická zpráva informující Vás, že úloha '%1%' byla provedena na stránce '%2%' @@ -593,16 +1063,70 @@

Mějte hezký den!

- Zdraví umbraco robot + Zdraví Umbraco robot

]]>
+ Byly změněny následující jazyky:

+ %0% + ]]>
[%0%] Upozornění o %1% na %2% Upozornění + Akce + Vytvořeno + Vytvořit balíček - a výběrem balíčku. Balíčky umbraco mají obvykle přípony ".umb" nebo ".zip". + a výběrem balíčku. Balíčky Umbraco mají obvykle přípony ".umb" nebo ".zip". ]]> + Tím se balíček odstraní + Přetáhněte sem pro nahrání + Zahrnout všechny podřízené uzly + nebo kliknutím sem vyberte soubor balíčku + Nahrát balíček + Nainstalujte místní balíček výběrem ze svého počítače. Instalujte pouze balíčky ze zdrojů, které znáte a kterým důvěřujete + Nahrát další balíček + Zrušit a nahrát další balíček + Přijímám + podmínky použití + Cesta k souboru + Absolutní cesta k souboru (ie: /bin/umbraco.bin) + Nainstalováno + Nainstalované balíčky + Instalovat místní + Dokončit + Tento balíček nemá žádné zobrazení konfigurace + Zatím nebyly vytvořeny žádné balíčky + Nemáte nainstalované žádné balíčky + Balíčky v pravém horním rohu obrazovky.]]> + Akce balíčku + Web autora + Obsah balíčku + Soubory balíčku + URL ikony + Nainstalovat balíček + Licence + URL licence + Vlastnosti balíčku + Hledat balíčky + Výsledky pro + Nemohli jsme nic najít + Zkuste prosím vyhledat jiný balíček nebo procházet jednotlivé kategorie + Oblíbené + Nové + + karma body + Informace + Vlastník + Přispěvatelé + Vytvořeno + Aktuální verze + .NET verze + Počet stažení + Počet lajků + kompatibilita + Tento balíček je kompatibilní s následujícími verzemi Umbraco, jak ohlásili členové komunity. Plnou kompatibilitu nelze zaručit u verzí hlášených pod 100% + Externí zdroje Autor Dokumentace Meta data balíčku @@ -621,6 +1145,17 @@ Upozornění: všechny dokumenty, media atd. závislé na položkách, které odstraníte, přestanou pracovat a mohou vést k nestabilitě systému, takže odinstalovávejte opatrně. Jste-li na pochybách, kontaktujte autora balíčku.]]>
Verze balíčku + Upgradování z verze + Balíček je již nainstalován + Tento balíček nelze nainstalovat, vyžaduje minimální verzi Umbraco + Odinstalovávám... + Stahuji... + Importuji... + Instaluji... + Restartuji, prosím čekejte... + Vše je hotovo, váš prohlížeč se nyní obnoví, prosím čekejte... + Klepnutím na tlačítko „Dokončit“ dokončete instalaci a znovu načtěte stránku. + Nahrávám balíček... Vložit s úplným formatováním (nedoporučeno) @@ -629,9 +1164,9 @@ Vložit, ale odstranit formátování (doporučeno) - Ochrana prostřednictvím rolí - použijte členské skupiny umbraca.]]> - Musíte vytvořit členskou skupinu před tím, než můžete použít autentizaci prostřednictvím rolí + Ochrana prostřednictvím rolí + použijte členské skupiny umbraca.]]> + Musíte vytvořit členskou skupinu před tím, než můžete použít autentizaci prostřednictvím rolí Chybová stránka Použita, když jsou lidé přihlášení, ale nemají přístup Vyberte, jak omezit přístup k této stránce @@ -640,16 +1175,28 @@ Přihlašovací stránka Vyberte stránku, která obsahuje přihlašovací formulář Odstranit ochranu + %0%?]]> Vyberte stránky, které obsahují přihlašovací formulář a chybová hlášení Vyberte role, které mají přístup k této stránce + %0%]]> + %0%]]> + Ochrana konkrétních členů + Pokud si přejete udělit přístup konkrétním členům Nastavte přihlašovací jmého a heslo pro tuto stránku Jednouživatelská ochrana Jestliže chcete nastavit jenom jednoduchou ochranu prostřednictvím uživatelského jména a hesla + Nedostatečná uživatelská oprávnění k publikování všech potomků + + @@ -659,7 +1206,9 @@ - Zahrnout nepublikované podřízené stránky + + + Ověření se nezdařilo pro požadovaný jazyk '% 0%'. Tento jazyk byl uložen, ale nezveřejněn. Probíhá publikování - počkejte, prosím... %0% ze %1% stránek bylo publikováno... %0% byla publikována @@ -668,23 +1217,49 @@ ok pro publikování %0% a tedy zveřejnění jejího obsahu.

Můžete publikovat tuto stránku a všechny její podstránky zatrhnutím publikovat všchny podstránky níže. ]]>
+ Zahrnout nepublikované podřízené stránky Nenakonfigurovali jste žádné schválené barvy + + Můžete vybrat pouze položky typu (typů): %0% + Vybrali jste aktuálně odstraněnou položku obsahu nebo položku v koši + Vybrali jste aktuálně odstraněné položky obsahu nebo položky v koši + + + Smazaná položka + Vybrali jste aktuálně odstraněnou položku média nebo položku v koši + Vybrali jste aktuálně odstraněné položky médií nebo položky médií v koši + V koši + + zadejte externí odkaz + zvolte interní stránku + Nadpis + Odkaz + Otevřít v novém okně + zadejte titulek + Zadejte odkaz + Přidat vnější odkaz Přidat vnitřní odkaz Přidat - Nadpis Vnitřní stránka URL Posunout dolů Posunout nahoru - Otevřít v novém okně Odebrat odkaz + + Zrušit oříznutí + Uložit oříznutí + Přidat nové oříznutí + Hotovo + Vrátit změny + + Vyberte verzi, kterou chcete porovnat s aktuální verzí Současná verze Červený text nebude ve vybrané verzi zobrazen, zelený znamená přidaný].]]> Dokument byl vrácen na starší verzi @@ -697,20 +1272,29 @@ Editovat skriptovací soubor - Domovník Obsah + Formuláře + Média + Členové + Balíčky + Nastavení + Překlad + Uživatelé + + Domovník Kurýr Vývojář Průvodce nastavením Umbraca - Media - Členové Zpravodaje - Nastavení Statistiky - Překlad - Uživatelé Nápověda + + Příručky + Nejlepší videopříručky Umbraco + Navštívit our.umbraco.com + Navštívit umbraco.tv + Výchozí šablona Pro importování typu dokumentu vyhledejte soubor ".udt" ve svém počítači tak, že kliknete na tlačítko "Prohledat" a pak kliknete na "Import" (na následující obrazovce budete vyzváni k potvrzení) @@ -724,18 +1308,26 @@ Záložky Nadřazený typ obsahu povolen Tento typ obsahu používá - jako nadřazený typ obsahu. Záložky z nadřazených typů obsahu nejsou zobrazeny a mohou byt editovány pouze na nadřazených typech obsahu samotných Na této záložce nejsou definovány žádné vlastnosti. Pro vytvoření nové vlastnosti klikněte na odkaz "přidat novou vlastnost" nahoře. + Vytvořit odpovídající šablonu + Přidat ikonu - Sort order - Creation date + Řazení + Datum vytvoření Třídění bylo ukončeno. Abyste nastavili, jak mají být položky seřazeny, přetáhněte jednotlivé z nich nahoru či dolů. Anebo klikněte na hlavičku sloupce pro setřídění celé kolekce + Tato položka nemá vnořené položky k seřazení - Publikování bylo zrušeno doplňkem třetí strany + Validace + Před uložením položky je nutné opravit chyby + Chyba + Uloženo + Nedostatečná uživatelská oprávnění, operace nemohla být dokončena + Zrušeno + Operace byla zrušena doplňkem třetí strany Typ vlastnosti už existuje Typ vlastnosti vytvořen Datový typ: %1%]]> @@ -749,108 +1341,332 @@ Stylopis byl uložen bez chyb Datový typ byl uložen Položka slovníku byla uložena - Publikování se nezdařilo, protože nadřazená stránka není publikována Obsah byl publikován a je viditelný na webu + %0% dokumentů zveřejněných a viditelných na webu + %0% zveřejněných a viditelných na webu + %0% dokumentů zveřejněných pro jazyky %1% a viditelných na webu Obsah byl uložen Nezapomeňte na publikování, aby se změny projevily + Načasování publikování bylo aktualizováno + %0% uloženo Odeslat ke schválení Změny byly odeslány ke schválení - Medium bylo uloženo - Medium bylo uloženo bez chyb + %0% změn bylo odesláno ke schválení + Médium bylo uloženo + Médium bylo uloženo bez chyb Člen byl uložen + Skupina členů byla uložena Vlastnost stylopisu byla uložena Stylopis byl uložen Šablona byla uložena Chyba při ukládání uživatele (zkontrolujte log) Uživatel byl uložen Typ uživatele byl uložen + Skupina uživatelů byla uložena + Jazyky a názvy hostitelů byly uloženy + Při ukládání jazyků a názvů hostitelů došlo k chybě Soubor nebyl uložen soubor nemohl být uložen. Zkontrolujte, prosím, oprávnění k souboru Soubor byl uložen Soubor byl uložen bez chyb Jazyk byl uložen + Typ média byl uložen + Typ člena byl uložen + Skupina členů byla uložena Šablona nebyla uložena Ujistěte se, prosím, že nemáte 2 šablony se stejným aliasem Šablona byla uložena Šablona byla uložena bez chyb! Publikování obsahu bylo zrušeno + Varianta obsahu %0% nebyla publikována + Povinný jazyk '%0%' nebyl publikován. Všechny jazyky pro tuto položku obsahu nejsou nyní publikovány. Částečný pohled byl uložen Částečný pohled byl uložen bez chyb! Částečný pohled nebyl uložen Při ukládání souboru došlo k chybě. + Oprávnění byla uložena pro + Smazáno %0% skupin uživatelů + %0% bylo smazáno + Povoleno %0% uživatelů + Zakázáno %0% uživatelů + %0% je nyní povoleno + %0% je nyní zakázáno + Skupiny uživatelů byly nastaveny + Odemčeno %0% uživatelů + %0% je nyný odemčeno + Člen byl exportován do souboru + Při exportu člena došlo k chybě + Uživatel %0% byl smazán + Pozvat uživatele + Pozvánka byla znovu odeslána na %0% + Dokument nelze publikovat, protože %0% není publikována + Ověření pro jazyk '%0%' se nezdařilo + Typ dokumentu byl exportován do souboru + Při exportu typu dokumentu došlo k chybě + Datum vydání nemůže být v minulosti + Nelze naplánovat publikování dokumentu, protože %0% není publikována + Dokument nelze naplánovat na publikování, protože „%0%“ má datum zveřejnění později než nepovinný jazyk + Datum vypršení platnosti nemůže být v minulosti + Datum vypršení nemůže být před datem vydání + + Publikování bylo zrušeno doplňkem třetí strany + Publikování se nezdařilo, protože nadřazená stránka není publikována - Používá CSS syntaxi např.: h1, .redHeader, .blueTex + Přidat styl + Upravit styl + Styly Rich Text editoru + Definujte styly, které by měly být k dispozici v editoru formátovaného textu pro tuto šablonu stylů Editovat stylopis Editovat vlastnost stylopisu Název, který identifikuje vlastnost stylu v editoru formátovaného textu Náhled + Jak bude text vypadat v Rich Text editoru. + CSS identifikátor nebo třída + Používá syntaxi CSS, např. "h1" nebo ".redHeader" Styly + CSS, který by měl být použit v editoru RTF, např. "color:red;" + Kód + Rich Text editor + + Používá CSS syntaxi např.: h1, .redHeader, .blueTex + Nepodařilo se odstranit šablonu s ID %0% Editovat šablonu + Sekce Vložit obsahovou oblast Vložit zástupce obsahové oblasti + Vložit + Vyberte, co chcete vložit do své šablony Vložit položku slovníku + Položka slovníku je zástupný symbol pro překladatelný text, což usnadňuje vytváření návrhů pro vícejazyčné webové stránky. Vložit makro + + Makro je konfigurovatelná součást, která je skvělá pro opakovaně použitelné části návrhu, kde potřebujete předat parametry, jako jsou galerie, formuláře a seznamy. + Vložit pole stránky umbraco + Zobrazuje hodnotu pojmenovaného pole z aktuální stránky s možnostmi upravit hodnotu nebo alternativní hodnoty. + Částečná šablona + + Částečná šablona je samostatný soubor šablony, který lze vykreslit uvnitř jiné šablony. Je to skvělé pro opakované použití nebo pro oddělení složitých šablon. + Nadřazená šablona - Rychlá příručka k šablonovým značkám umbraca + Žádný master + Vykreslit podřízenou šablonu + @RenderBody(). + ]]> + Definujte pojmenovanou sekci + @section {...}. Ta může být vykreslena v konkrétní oblasti nadřazené šablony pomocí @RenderSection. + ]]> + Vykreslit pojmenovanou sekci + @RenderSection(name). Tím se vykreslí oblast podřízené šablony, která je zabalena do odpovídající definice @section[name] {...}. + ]]> + Název sekce + Sekce je povinná + @section, jinak se zobrazí chyba. + ]]> + Tvůrce dotazů + položky vráceny, do + zkopírovat do schránky + Chci + veškerý obsah + obsah typu "%0%" + z(e) + můj web + kde + a + je + není + před + před (včetně zvoleného datumu) + po + po (včetně zvoleného datumu) + rovná se + nerovná se + obsahuje + neobsahuje + větší než + větší nebo rovno + menší než + menší nebo rovno + Id + Název + Datum vytvoření + Datum poslední aktualizace + řadit podle + vzestupně + sestupně Šablona + + Rychlá příručka k šablonovým značkám umbraca - Image - Macro - 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 - Allow all editors - Allow all row configurations + Obrázek + Makro + Vybrat typ obsahu + Vybrat rozvržení + Přidat řádek + Přidat obsah + Zahodit obsah + Nastavení aplikováno + Tento obsah zde není povolen + Tento obsah je zde povolen + Klepněte pro vložení + Klepnutím vložíte obrázek + Titulek obrázku... + Zde pište... + Rozvržení mřížky + Rozvržení je celková pracovní oblast pro editor mřížky, obvykle potřebujete pouze jedno nebo dvě různá rozvržení + Přidat rozvržení mřížky + Upravte rozvržení nastavením šířky sloupců a přidáním dalších sekcí + Konfigurace řádků + Řádky jsou předdefinované buňky uspořádané vodorovně + Přidat konfiguraci řádku + Upravte řádek nastavením šířky buněk a přidáním dalších buněk + Sloupce + Celkový počet sloupců v rozvržení mřížky + Nastavení + Nakonfigurujte, jaká nastavení mohou editoři změnit + Styly + Nakonfigurujte, co mohou editoři stylů změnit + Povolit všechny editory + Povolit všechny konfigurace řádků + Maximální počet položek + Nechte prázdné nebo nastavte na 0 pro neomezené + Nastavit jako výchozí + Vyberat navíc + Zvolit výchozí + jsou přidány + Varování + Odstraňujete konfiguraci řádku + + Odstranění názvu konfigurace řádku povede ke ztrátě dat pro veškerý existující obsah založený na této konfiguraci. + + + + Složení + Skupina + Nepřidali jste žádné skupiny + Přidat skupinu + Zděděno od + Přidat vlastnost + Požadovaný popisek + Povolit zobrazení seznamu + Nakonfiguruje položku obsahu tak, aby zobrazovala seznam svých potomků a seznam potomků, které je možné prohledávat, potomci se nebudou zobrazovat ve stromu + Povolené šablony + Vyberte, kteří editoři šablon mohou používat obsah tohoto typu + Povolit jako root + Povolit editorům vytvářet obsah tohoto typu v kořenovém adresáři stromu obsahu. + Povolené typy podřízených uzlů + Povolit vytváření obsahu zadaných typů pod obsahem tohoto typu. + Vybrat podřízený uzel + Zdědí záložky a vlastnosti z existujícího typu dokumentu. Nové záložky budou přidány do aktuálního typu dokumentu nebo sloučeny, pokud existuje záložka se stejným názvem. + Tento typ obsahu se používá ve složení, a proto jej nelze poskládat. + Nejsou k dispozici žádné typy obsahu, které lze použít jako složení. + Odebráním složení odstraníte všechna související data vlastností. Jakmile uložíte typ dokumentu, již není cesta zpět. + Vytvořit nové + Použít existující + Nastavení editoru + Konfigurace + Ano, smazat + bylo přesunuto pod + bylo zkopírováno pod + Vybrat složku, kterou chcete přesunout + Vybrat složku, kterou chcete kopírovat + ve stromové struktuře níže + Všechny typy dokumentů + Všechny dokumenty + Všechny média + použití tohoto typu dokumentu bude trvale smazáno, prosím potvrďte, že je chcete také odstranit. + použití tohoto typu média bude trvale smazáno, potvrďte, že je chcete také odstranit. + použití tohoto typu člena bude trvale smazáno, potvrďte, že je chcete také odstranit + a všechny dokumenty používající tento typ + a všechny mediální položky používající tento typ + a všichni členové používající tento typ + Člen může upravovat + Povolit editaci této vlastnosti členem na jeho stránce profilu + Obsahuje citlivá data + Skrýt tuto hodnotu vlastnosti před editory obsahu, kteří nemají přístup k prohlížení citlivých informací + Zobrazit v profilu člena + Povolit zobrazení této vlastnosti na stránce profilu člena + záložka nemá žádné řazení + Kde se toto složení používá? + Toto složení se v současnosti používá ve složení následujících typů obsahu: + Povolit různé jazyky + Povolit editorům vytvářet obsah tohoto typu v různých jazycích. + Povolit různé jazyky + Typ prvku + Je typ prvku + Typ prvku je určen k použití například ve vnořeném obsahu, nikoli ve stromu. + Jakmile byl typ dokumentu použit k vytvoření jedné nebo více položek obsahu, nelze jej změnit na typ prvku. + To neplatí pro typ prvku + V této vlastnosti jste provedli změny. Opravdu je chcete zahodit? + + + Přidat jazyk + Povinný jazyk + Před publikováním uzlu je nutné vyplnit vlastnosti v tomto jazyce. + Výchozí jazyk + Web Umbraco může mít nastaven pouze jeden výchozí jazyk. + Přepnutí výchozího jazyka může mít za následek chybějící výchozí obsah. + Nahradit nepřeložený obsah za + Žádné nahrazení nepřeloženého jazyka + Chcete-li povolit automatické zobrazení vícejazyčného obsahu v jiném jazyce, pokud není v požadovaném jazyce přeložen, vyberte jej zde. + Nahrazujicí jazyk + žádný + + + Přidat parametr + Upravit parametr + Zadejte název makra + Parametry + Definujte parametry, které by měly být k dispozici při použití tohoto makra. + Vyberte soubor makra pro částečnou šablonu + + + Stavební modely + to může chvíli trvat, nebojte se + Generované modely + Modely nelze vygenerovat + Generování modelů selhalo, viz výjimka v logu Umbraca + Přidat záložní pole + Náhradní pole + Přidat výchozí hodnotu + Výchozí hodnota Alternativní pole Alternativní text Velká a malá písmena Kódování Vybrat pole - Konvertovat + Konvertovat + Ano, převést konce řádků Nahrazuje nové řádky html tagem &lt;br&gt; Vlastní pole Ano, pouze datum + Formát a kódování Formátovat jako datum + Naformátuje hodnotu jako datum nebo datum s časem podle aktivního jazyka HTML kódování Nahradí speciální znaky jejich HTML ekvivalentem. Bude vloženo za hodnotou pole Bude vloženo před hodnotou pole Malá písmena + Upravit výstup Nic + Ukázka výstupu Vložit za polem Vložit před polem Rekurzivní + Ano, udělej to rekurzivní + Oddělovač Standardní pole Velká písmena Kódování URL @@ -861,7 +1677,7 @@ Podrobnosti překladu - Stáhnout xml DTD + Stáhnout XML DTD Pole Zahrnout podstránky Žádní uživatelé překladatelé nebyli nalezeni. Vytvořte, prosím, překladatele před tím, než začnete posílat obsah k překladu Stránka '%0%' byla poslána k překladu @@ -892,6 +1708,9 @@ Nahrát xml překladu + Obsah + Šablony obsahu + Média Prohlížeč mezipaměti Koš Vytvořené balíčky @@ -909,8 +1728,11 @@ Role Typy členů Typy dokumentů + Typy vztahů/vazeb Balíčky Balíčky + Částečné šablony + Makra částečných šablon Instalovat z úložiště Instalovat Runway Moduly Runway @@ -918,9 +1740,14 @@ Skripty Stylopisy Šablony - Oprávnění Uživatele - Typy Uživatelů + Prohlížeč logu Uživatelé + Nastavení + Šablony + Třetí strana + + Oprávnění uživatele + Typy uživatelů Nová aktualizace je připrvena @@ -929,23 +1756,46 @@ Chyba při kontrole aktualizace. Zkontrolujte, prosím, trasovací zásobník pro další informace + Přístupy + Na základě přiřazených skupin a počátečních uzlů má uživatel přístup k následujícím uzlům + Přiřadit přístup Administrátor Pole kategorie + Uživatel byl vytvořen Změnit heslo + Změnit fotku Změnit heslo + nebyl uzamčen + Heslo nebylo změněno Potvrdit heslo Můžete změnit své heslo pro přístup do administrace Umbraca vyplněním formuláře níže a kliknutím na tlačítko 'Změnit Heslo' Kanál obsahu + Vytvořit dalšího uživatele + Vytvořte nové uživatele a udělte mu přístup do Umbraco. Po vytvoření nového uživatele bude vygenerováno heslo, které s ním můžete sdílet. Popis Deaktivovat uživatele Typ dokumentu Editor Výtah + Neúspěšné pokusy o přihlášení + Přejít na uživatelský profil + Přidáním skupin přidělte přístup a oprávnění + Pozvat dalšího uživatele + Pozvěte nové uživatele, a poskytněte jim přístup do Umbraco. Uživatelům bude zaslán e-mail s pozvánkou a s informacemi o tom, jak se přihlásit do Umbraco. Pozvánky mají platnost 72 hodin. Jazyk + Nastavte jazyk, který uvidíte v nabídkách a dialogových oknech + Poslední datum uzamčení + Poslední přihlášení + Heslo bylo naposledy změněno Přihlašovací jméno Úvodní uzel v knihovně medií + Omezte knihovnu médií na konkrétní počáteční uzel + Úvodní uzly v knihovně medií + Omezte knihovnu médií na konkrétní počáteční uzly Sekce Deaktivovat přistup k Umbracu + se dosud nepřihlásil + Staré heslo Heslo Resetovat heslo Vyše heslo bylo změněno! @@ -959,13 +1809,528 @@ Nahradit oprávnění podřízených uzlů Nyní měníte oprávnění pro stránky: Vyberte stránky, pro které chcete měnit oprávnění + Odebrat fotografii + Výchozí oprávnění + Upřesnění oprávnění + Nastavte oprávnění pro konkrétní uzly + Profil Prohledat všechny podřízené uzly + Přidejte sekce, do kterých mají uživatelé přístup + Vybrat skupiny uživatelů + Nebyl vybrán žádný počáteční uzel + Nebyly vybrány žádné počáteční uzly Úvodní uzel v obsahu + Omezte strom obsahu na konkrétní počáteční uzel + Úvodní uzly obsahu + Omezte strom obsahu na konkrétní počáteční uzly + Uživatel byl naposledy aktualizován + byl vytvořen + Nový uživatel byl úspěšně vytvořen. Pro přihlášení do Umbraco použijte heslo níže. + Správa uživatelů Uživatelské jméno Oprávnění uživatele + Uživatelská skupina + byl pozván + Novému uživateli byla zaslána pozvánka s informacemi, jak se přihlásit do Umbraco. + Dobrý den, vítejte v Umbraco! Za pouhou 1 minutu budete moci používat Umbraco. Jenom od vás potřebujeme, abyste si nastavili heslo a přidali obrázek pro svůj avatar. + Vítejte v Umbraco! Vaše pozvánka bohužel vypršela. Obraťte se na svého správce a požádejte jej, aby jí znovu odeslal. + Nahrání vaší fotografie usnadní ostatním uživatelům, aby vás poznali. Kliknutím na kruh výše nahrajte svou fotku. Spisovatel + Změnit Váš profil Vaše nedávná historie Relace vyprší za + Pozvat uživatele + Vytvořit uživatele + Odeslat pozvánku + Zpět na seznam uživatelů + Umbraco: Pozvánka + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Zdravím Vás, %0%, +

+

+ Byli jste pozváni %1% do CMS Umbraco. +

+

+ Zpráva od %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Kliknutím na tento odkaz přijměte pozvání + +
+
+

Pokud nemůžete kliknout na odkaz, zkopírujte a vložte tuto adresu URL do okna prohlížeče:

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

+
+
+


+
+
+ + ]]>
+ Pozvat + Zasílám pozvání... + Smazat uživatele + Opravdu chcete smazat tento uživatelský účet? + Vše + Aktivní + Zakázané + Uzamčeno + Pozváno + Neaktivní + Jméno (A-Z) + Jméno (Z-A) + Nejnovější + Nejstarší + Poslední přihlášení + Nebyly přidány žádné skupiny uživatelů + + + Validace + Ověřit jako e-mailovou adresu + Ověřit jako číslo + Ověřit jako URL + ...nebo zadat vlastní ověření + Pole je povinné + Zadat chybovou zprávu pro vlastní validaci (volitelné) + Zadat regulární výraz + Zadat chybovou zprávu pro vlastní validaci (volitelné) + Musíte přidat alespoň + Můžete jen mít + položky + vybrané položky + Neplatné datum + Není číslo + Neplatný e-mail + Hodnota nemůže být nulová + Hodnota nemůže být prázdná + Hodnota je neplatná, neodpovídá správnému vzoru + Vlastní ověření + %1% více.]]> + %1% příliš mnoho.]]> + + + + Hodnota je nastavena na doporučenou hodnotu: '%0%'. + Hodnota byla nastavena na '%1%' pro XPath '%2%' v konfiguračním souboru '%3%'. + Očekávaná hodnota '%1%' pro '%2%' v konfiguračním souboru '%3%', ale nalezeno '%0%'. + Nalezena neočekávaná hodnota '%0%' pro '%2%' v konfiguračním souboru '%3%'. + + Vlastní chyby jsou nastaveny na '%0%'. + Vlastní chyby jsou aktuálně nastaveny na '%0%'. Před nasazením se doporučuje nastavit na '%1%'. + Vlastní chyby byly úspěšně nastaveny na '%0%'. + MacroErrors jsou nastaveny na '%0%'. + MakroErrors jsou nastaveny na '%0%', což zabrání úplnému načtení některých nebo všech stránek na vašem webu, pokud dojde k chybám v makrech. Náprava nastaví hodnotu na '%1%'. + MakroErrors jsou nyní nastaveny na '%0%'. + + Try Skip IIS Custom Errors je nastaveno na '%0%' a používáte verzi IIS '%1%'. + Try Skip IIS Custom Errors je aktuálně nastaveno na '%0%'. Doporučuje se nastavit %1% pro vaši verzi služby IIS (%2%). + Try Skip IIS Custom Errors úspěšně nastaveno na '%0%'. + + Soubor neexistuje: '%0%'. + '% 0%' v konfiguračním souboru '% 1%'.]]> + Došlo k chybě, zkontrolujte ji v logu: %0%. + Databáze - Databázové schéma je pro tuto verzi Umbraco správné + Bylo zjištěno %0% problémů se schématem vaší databáze (podrobnosti najdete v logu) + Při ověřování databázového schématu vůči aktuální verzi Umbraco byly zjištěny některé chyby. + Certifikát vašeho webu je platný. + Chyba ověření certifikátu: '%0%' + Platnost SSL certifikátu vašeho webu vypršela. + Platnost certifikátu SSL vašeho webu vyprší za %0% dní. + Chyba při pingování adresy URL %0% - '%1%' + Aktuálně prohlížíte web pomocí schématu HTTPS. + AppSetting 'Umbraco.Core.UseHttps' je v souboru web.config nastaven na 'false'. Jakmile vstoupíte na tento web pomocí schématu HTTPS, mělo by být nastaveno na 'true'. + AppSetting 'Umbraco.Core.UseHttps' je v souboru web.config nastaven na '%0%', vaše cookies %1% jsou označeny jako zabezpečené. + V souboru web.config se nepodařilo aktualizovat nastavení 'Umbraco.Core.UseHttps'. Chyba: %0% + + Povolit HTTPS + Nastaví nastavení umbracoSSL na true v appSettings v souboru web.config. + AppSetting 'Umbraco.Core.UseHttps' je nyní nastaveno na 'true' v souboru web.config, vaše cookies budou označeny jako zabezpečené. + Fix + Nelze opravit kontrolu pro porovnání hodnot pomocí 'ShouldNotEqual'. + Nelze opravit kontrolu pro porovnání hodnot pomocí 'ShouldEqual' s poskytnutou hodnotou. + Hodnota k opravě nebyla poskytnuta. + Režim kompilace ladění je zakázán. + Režim ladění je aktuálně povolen. Před spuštěním webu se doporučuje toto nastavení deaktivovat. + Režim ladění byl úspěšně deaktivován. + Režim sledování je deaktivován. + Režim sledování je aktuálně povolen. Před spuštěním se doporučuje toto nastavení deaktivovat. + Režim sledování byl úspěšně deaktivován. + Všechny složky mají nastavena správná oprávnění. + + %0%.]]> + %0%. Pokud nejsou psány, není třeba podniknout žádné kroky.]]> + Všechny soubory mají nastavena správná oprávnění. + + %0%.]]> + %0%. Pokud nejsou psány, není třeba podniknout žádné kroky.]]> + X-Frame-Options, které určuje, zda může být obsah webu zobrazen na jiném webu pomocí IFRAME.]]> + X-Frame-Options, které určuje, zda může být obsah webu zobrazen na jiném webu pomocí IFRAME.]]> + Nastavit záhlaví v Konfiguraci + Přidá hodnotu do sekce httpProtocol/customHeaders do web.config, aby se zabránilo tomu, že web může být zobrazen na jiném webu pomocí IFRAME. + Do souboru web.config bylo přidáno nastavení pro vytvoření záhlaví, které zabrání jinému webu, zobrazit tento web pomocí IFRAME. + Nelze aktualizovat soubor web.config. Chyba: %0% + X-Content-Type-Options použitá k ochraně před zranitelnostmi čichání MIME.]]> + X-Content-Type-Options použité k ochraně před zranitelnostmi čichání MIME nebyly nalezeny.]]> + Přidá hodnotu do sekce httpProtocol/customHeaders v souboru web.config, která chrání před zranitelnostmi MIME. + Do souboru web.config bylo přidáno nastavení pro vytvoření záhlaví, které chrání před zranitelnostmi MIME. + Strict-Transport-Security, také známo jako HSTS-header, bylo nalezeno.]]> + Strict-Transport-Security nebylo nalezeno.]]> + Do sekce httpProtocol/customHeaders v souboru web.config přidá záhlaví 'Strict-Transport-Security' s hodnotou 'max-age = 10886400'. Tuto opravu použijte pouze v případě, že vaše domény budou spuštěny s https po dobu příštích 18 týdnů (minimálně). + Do vašeho souboru web.config bylo přidáno záhlaví HSTS. + X-XSS-Protection bylo nalezeno.]]> + X-XSS-Protection bylo nalezeno.]]> + Přidá záhlaví 'X-XSS-Protection' s hodnotou '1; mode=block' do sekce httpProtocol/customHeaders v souboru web.config. + Záhlaví X-XSS-Protection bylo přidáno do vašeho souboru web.config. + + %0%.]]> + Nebyly nalezeny žádné hlavičky odhalující informace o technologii webových stránek. + V souboru Web.config nelze najít system.net/mailsettings. + V části system.net/mailsettings v souboru web.config není hostitel nakonfigurován. + Nastavení SMTP jsou správně nakonfigurována a služba funguje jak má. + Server SMTP konfigurovaný s hostitelem '%0%' a portem '%1%' nelze nalézt. Zkontrolujte prosím, zda jsou nastavení SMTP v souboru Web.config a v sekci system.net/mailsettings správná. + %0%.]]> + %0%.]]> +

Výsledky plánovaných kontrol Umbraco Health Checks provedených na %0% v %1% jsou následující:

%2%]]>
+ Stav Umbraco Health Check: %0% + Zkontrolovat všechny skupiny + Zkontrolovat skupinu + + Kontrola vyhodnocuje různé oblasti vašeho webu z hlediska nastavení osvědčených postupů, konfigurace, potenciálních problémů atd. Problémy lze snadno vyřešit stisknutím tlačítka. Můžete přidat své vlastní kontroly, podívejte se na dokumentaci pro více informací o vlastních kontrolách.

+ ]]> +
+ + + Zakázat sledování URL + Povolit sledování URL + Jazyk + Originální URL + Přesměrováno na + Správa URL přesměrování + Na tuto položku obsahu přesměrovávají následující adresy URL: + Nebyla provedena žádná přesměrování + Jakmile bude publikovaná stránka přejmenována nebo přesunuta, bude automaticky provedeno přesměrování na novou stránku. + Opravdu chcete odstranit přesměrování z '%0%' na '%1%'? + Přesměrování bylo odstraněno. + Chyba při odebírání URL přesměrování. + Toto odstraní přesměrování + Opravdu chcete zakázat sledování URL adres? + Sledování URL adres je nyní zakázáno. + Při deaktivaci sledování URL adres došlo k chybě, další informace naleznete v logu. + Sledování URL adres je nyní povoleno. + Chyba při povolení sledování URL adres, další informace lze nalézt v logu. + + + Žádné položky ze slovníku na výběr + + + %0% znaků.]]> + %1% je moc.]]> + + + Obsah s ID: {0} v koši souvisí s původním nadřazeným obsahem s ID: {1} + Média s ID: {0} v koši souvisí s původním nadřazeným médiem s ID: {1} + Tuto položku nelze automaticky obnovit + Neexistuje žádné místo, kde lze tuto položku automaticky obnovit. Položku můžete přesunout ručně pomocí stromu níže. + byla obnovena pod + + + Směr + Nadřazený s potomkem + Obousměrný + Nadřazená + Potomek + Počet + Vazby + Vytvořeno + Komentář + Název + Žádné vazby pro tento typ vazby. + Typ vazby + Vazby + + + Začínáme + Správa přesměrování + Obsah + Vítejte + Správa Examine + Stav publikování + Tvůrce modelů + Health Check + Profilování + Začínáme + Instalovat Umbraco formuláře + + + Jít zpět + Aktivní rozvržení: + Skočit do + skupina + prošlo + varování + selhalo + návrh + Kontrola prošla + Kontrola selhala + Otevřít hledání v backoffice + Otevřít/zavřít nápovědu backoffice + Otevřít/zavřít možnosti vašeho profilu + Otevřít kontextové menu pro + Aktuální jazyk + Přepnout jazyk na + Vytvořit novou složku + Částečná šablona + Makro částečné šablony + Člen + Datový typ + Prohledat přesměrování + Prohledat skupiny uživatelů + Prohledat uživatele + Vytvořit položku + Vytvořit + Upravit + Název + + + Závislosti + Tento datový typ nemá žádné závislosti. + Použito v dokumentových typech + Žádné vazby na typy dokumentů. + Použito v typech médií + Žádné vazby na typy médií. + Použito v typech členů + Žádné vazby na typy členů. + Použito v + Použito v dokumentech + Použito ve členech + Použito v médiích + + + Úrovně logování + Vybrat vše + Odznačit vše + Uložená vyhledávání + Celkem položek + Časové razítko + Úroveň + Stroj + Zpráva + Výjimka + Vlastnosti + Vyhledat na Googlu + Vyhledat zprávu na Googlu + Vyhledat na Bing + Vyhledat zprávu na Bing + Prohledat naše Umbraco + Vyhledat tuto zprávu na našich fórech a dokumentech Umbraco + Vyhledat Our Umbraco na Googlu + Prohledat Our Umbraco fóra pomocí Googlu + Prohledat Umbraco Source + Vyhledat ve zdrojovém kódu Umbraco na Github + Prohledat Umbraco Issues + Prohledat Umbraco Issues na Github + Smazat toto vyhledávání + Najít logy s ID požadavku + Najít logy se jmenným prostorem + Najít logy s názvem stroje + Otevřít + + + Kopírovat %0% + %0% z %1% + Odebrat všechny položky + + + Otevřít akce vlastností + + + Čekejte + Stav obnovení + Cache paměť + + + + Znovu načíst + Cache databáze + + Znovuvytvoření může být náročné. Použijte jej, když nestačí obnovení stránky, a domníváte se, že mezipaměť databáze nebyla správně vygenerována - což by naznačovalo možný kritický problém Umbraco. + ]]> + + Obnovit + Internals + + nebudete muset používat. + ]]> + + Sběr + Stav publikované mezipaměti + Mezipaměti + + + Profilování výkonu + + Umbraco aktuálně běží v režimu ladění. To znamená, že můžete použít vestavěný profiler výkonu k vyhodnocení výkonu při vykreslování stránek.

Pokud chcete aktivovat profiler pro konkrétní vykreslení stránky, jednoduše při požadavku na stránku jednoduše přidejte umbDebug=true do URL.

Pokud chcete, aby byl profiler ve výchozím nastavení aktivován pro všechna vykreslení stránky, můžete použít přepínač níže. Ve vašem prohlížeči nastaví soubor cookie, který automaticky aktivuje profiler. Jinými slovy, profiler bude ve výchozím nastavení aktivní pouze ve vašem prohlížeči, ne v ostatních.

+ ]]> +
+ Ve výchozím stavu aktivovat profiler + Přátelské připomenutí + + + Nikdy byste neměli nechat produkční web běžet v režimu ladění. Režim ladění je vypnut nastavením debug="false" na elementu compilation v souboru web.config. +

+ ]]> +
+ + + Umbraco v současné době neběží v režimu ladění, takže nemůžete použít vestavěný profiler. Takto by to mělo být pro produkční web. +

+

+ Režim ladění je zapnut nastavením debug="true" na elementu compilation v souboru web.config. +

+ ]]> +
+ + + Hodiny tréninkových videí Umbraco jsou blíž než si myslíte + + Chcete ovládnout Umbraco? Stačí strávit pár minut sledování jednoho z těchto videí o používání Umbraco. Nebo navštivte umbraco.tv, kde najdete ještě více videí o Umbraco

+ ]]> +
+ Chcete-li začít + + + Začněte zde + Tato část obsahuje stavební bloky pro váš web Umbraco. Podle níže uvedených odkazů se dozvíte více o práci s položkami v části Nastavení + Zjistit více + + v sekci Dokumentace v Our Umbraco + ]]> + + + fóru komunity + ]]> + + + výuková videa (některá jsou zdarma, jiná vyžadují předplatné) + ]]> + + + nástrojích zvyšujících produktivitu a komerční podpoře + ]]> + + + školení a certifikace + ]]> + + + + Vítejte v přátelském CMS + Děkujeme, že jste si vybrali Umbraco - myslíme si, že by to mohl být začátek něčeho krásného. I když se to může zpočátku zdát ohromující, udělali jsme hodně pro to, aby byla křivka učení co nejhladší a nejrychlejší. + + + Umbraco formuláře + Vytvářejte formuláře pomocí intuitivního rozhraní drag and drop. Od jednoduchých kontaktních formulářů, které odesílají e-maily, až po pokročilé dotazníky, které se integrují do systémů CRM. Vaši klienti to budou milovat! diff --git a/TestSite/Umbraco/Config/Lang/cy.xml b/TestSite/Umbraco/Config/Lang/cy.xml new file mode 100644 index 0000000..c479f27 --- /dev/null +++ b/TestSite/Umbraco/Config/Lang/cy.xml @@ -0,0 +1,2764 @@ + + + + Method4 Ltd + https://www.method4.co.uk/ + + + Diwylliannau ac Enwau Gwesteia + Trywydd Archwilio + Dewis Nod + Newid Math o Ddogfen + Newid Math o Data + Copïo + Creu + Allforio + Creu Pecyn + Creu grŵp + Dileu + Analluogi + Golygu gosodiadau + Gwagu bin ailgylchu + Galluogi + Allforio Math o Ddogfen + Mewnforio Math o Ddogfen + Mewnforio Pecyn + Golygu mewn Cynfas + Gadael + Symud + Hysbysiadau + Cyrchiad cyhoeddus + Cyhoeddi + Dadgyhoeddi + Ail-lwytho + Ail-gyhoeddi yr holl safle + Dileu + Ailenwi + Adfer + Gosod hawliau ar gyfer y dudalen %0% + Dewis ble i copïo + Dewis ble i symud + Yn y strwythyr goeden isod + Dewis ble i gopïo'r eitem(au) a ddewiswyd + Dewis ble i symud yr eitem(au) a ddewiswyd + wedi symud i + wedi copïo i + wedi dileu + Hawliau + Rolio yn ôl + Anfon I Gyhoeddi + Anfon I Gyfieithu + Gosod grŵp + Trefnu + Cyfieithu + Diweddaru + Gosod Hawliau + Datgloi + Creu Templed Gynnwys + Ail-anfon Gwahoddiad + + + Cynnwys + Gweinyddu + Strwythyr + Arall + + + Caniatáu hawl i osod to assign diwylliannau ac enwau gwesteia + Caniatáu hawl i weld cofnod hanes nod + Caniatáu hawl i weld nod + Caniatáu hawl i newid math o ddogfen ar gyfer nod + Caniatáu hawl i gopïo nod + Caniatáu hawl i greu nodau + Caniatáu hawl i ddileu nodau + Caniatáu hawl i symud nodau + Caniatáu hawl i osod a newid cyrchiad cyhoeddus ar gyfer nod + Caniatáu hawl i gyhoeddi nod + Caniatáu hawl i dadgyhoeddi nod + Caniatáu hawl i newid hawliau ar gyfer nod + Caniatáu hawl i rolio nod yn ôl at gyflwr blaenorol + Caniatáu hawl i anfon nod am gymeradwyo cyn cyhoeddi + Caniatáu hawl i anfon nod am gyfieithiad + Caniatáu hawl i newid trefn nodau + Caniatáu hawl i gyfiethu nod + Caniatáu hawl i achub nod + Caniatáu hawl i greu Templed Cynnwys + + + Cynnwys + Gwybodaeth + + + Dim hawl. + Ychwanegu Parth newydd + dileu + Nod annilys. + Fformat parth annilys. + Parth wedi'i neilltuo eisoes. + Iaith + Parth + Parth newydd '%0%' wedi'i greu + Parth '%0%' wedi dileu + Parth '%0%' wedi neilltuo eisoes + Parth '%0%' wedi diweddaru + Golygu Parthau Presennol + + + + Etifeddu + Diwylliant + + neu etifeddu diwylliant o nodau rhiant. Bydd hyn hefyd
+ yn berthnasol i'r nod bresennol, oni bai fod parth isod yn berthnasol hefyd.]]> +
+ Parthau + + + Clirio dewisiad + Dewis + Gwneud rhywbeth arall + Trwm + Canslo Mewnoliad Paragraff + Mewnosod maes ffurflen + Mewnosod pennawd graffig + Golygu Html + Mewnoli Paragraff + Italig + Canoli + Unioni Chwith + Unioni Dde + Mewnosod Dolen + Mewnosod dolen leol (angor) + Rhestr Bwled + Rhestr rhifol + Mewnosod macro + Mewnosod llun + Chyhoeddi a cau + Cyhoeddi efo disgynnydd + Golygu perthnasau + Dychwelyd i'r rhestr + Achub + Achub a cau + Achub a chyhoeddi + Achub ac amserlenni + Achub ac anfon am gymeradwyo + Achub gwedd rhestr + Amserlenni + Rhagolwg + Save and preview + Rhagolwg wedi analluogi gan nad oes templed wedi'i neilltuo + Dewis arddull + Dangos arddulliau + Mewnosod tabl + Cynhyrchu modelau a cau + Achub a chynhyrchu modelau + Dadwneud + Ail-wneud + Rolio yn ôl + Dileu tag + Canslo + Cadarnhau + Mwy opsiynau cyhoeddi + Submit + Submit and close + + + Dangos am + Cynnwys wedi'i dileu + Cynnwys wedi'i dadgyhoeddi + Cynnwys wedi'i dadgyhoeddi am y ieithoedd: %0% + Cynnwys wedi'i Achub a Chyhoeddi + Cynnwys wedi'i Achub a Chyhoeddi am y ieithoedd: %0% + Cynnwys wedi'i achub + Cynnwys wedi'i achub am y ieithoedd: %0% + Cynnwys wedi'i symud + Cynnwys wedi'i copïo + Cynnwys wedi'i rolio yn ôl + Cynnwys wedi'i anfon i Gyhoeddi + Cynnwys wedi'i anfon i gyhoeddi am y ieithoedd: %0% + Cynnwys wedi'i anfon i gyfieithu + Trefnu eitemau blant cyflawnwyd gan ddefnyddiwr + %0% + Copïo + Cyhoeddi + Cyhoeddi + Symud + Achub + Achub + Dileu + Dadgyhoeddi + Dadgyhoeddi + Rolio yn ôl + Anfon i Gyhoeddi + Anfon i Gyhoeddi + Anfon i Gyfieithu + Tefnu + Arferu + Hanes (pob amrywiad) + + + Methwyd creu ffolder o dan id rhiant %0% + Methwyd creu ffolder o dan rhiant efo enw %0% + Mae'r enw'r ffolder methu cynnwys nodau anghyfreithlon. + Methwyd dileu eitem: %0% + + + Wedi Cyhoeddi + Am y dudlaen yma + Enw arall + (sut fyddwch chi'n disgrifio'r llun dros y ffôn) + Dolenni Amgen + Cliwich i olygu'r eitem yma + Creuwyd gan + Awdur gwreiddiol + Diweddarwyd gan + Creuwyd + Dyddiad/amser creuwyd y ddogfen yma + Math o Ddogfen + Yn golygu + Dileu am + Mae'r eitem yma wedi cael ei newid ar ôl cyhoeddi + Nid yw'r eitem yma wedi cael ei gyhoeddi + Cyhoeddiad ddiwethaf + Nid oes unrhyw eitemau i ddangos + Nid oes unrhyw eitemau i ddangos yn y rhestr. + Nid oes unrhyw gynnwys wedi'i ychwanegu + Nid oes unrhyw aelodau wedi'u ychwanegu + Math o Gyfrwng + Dolen i eitem gyfrwng(au) + Grŵp Aelod + Rôl + Math o Aelod + Dim newidiadau wedi'u gwneud + Dim dyddiad wedi'i ddewis + Teitl tudalen + Does dim dolen gan yr eitem gyfrwng yma + Ni all unrhyw gynnwys cael ei hychwanegu am eitem hon + Priodweddau + Mae'r ddogfen yma wedi'i gyhoeddi ond nid yw'n weladwy gan nad yw'r rhiant '%0%' wedi'i gyhoeddi + Mae'r diwylliant yma yn cyhoeddedig ond ddim yn weladwy oherwydd mae'n anghyhoeddedig ar rhiant '%0%' + Mae'r ddogfen yma wedi'i gyhoeddi ond nid yw'n bodoli yn y storfa + Ni ellir nôl y url + Mae'r ddogfen yma wedi'i gyhoeddi ond byddai'r url yn gwrthdaro gyda chynnwys %0% + Mae'r ddogfen yma wedi'i gyhoeddi ond mae'r url methu cael ei cyfeirio + Cyhoeddi + Wedi cyhoeddi + Wedi cyhoeddi (newidiadau nes arddodiad) + Statws Cyhoeddi + Cyhoeddi efo disgynnyddion i cyhoeddi %0% ac yr holl eitemau cynnwys o dan ac a thrwy hynny wneud eu cynnwys ar gael i'r cyhoedd.]]> + Cyhoeddi efo disgynnyddion i cyhoeddi y ieithoedd a ddewiswyd ac yr un ieithoedd o'r eitemau o dan a thrwy hynny wneud eu cynnwys ar gael i'r cyhoedd.]]> + Cyhoeddi am + Dadgyhoeddi am + Clirio Dyddiad + Gosod dyddiad + Trefn wedi diweddaru + Er mwyn trefnu'r nodau, llusgwch y nodau neu cliciwch un o benynnau'r colofnau. Gallwch ddewis nifer o nodau gan ddal y botwm "shift" neu "control" wrth ddewis + Ystadegau + Teitl (dewisol) + Testyn amgen (dewisol) + Math + Dadgyhoeddi + Wedi dadgyhoeddi + Heb ei greu + Golygwyd ddiwethaf + Dyddiad/amser golygwyd y ddogfen yma + Dileu ffeil(iau) + Cliciwch yma i dileu'r llun oddi wrth y eitem cyfrwng + Cliciwch yma i dileu'r ffeil oddi wrth y eitem cyfrwng + Dolen i ddogfen + Aeold o grŵp(iau) + Ddim yn aelod o'r grŵp(iau) + Eitemau blentyn + Targed + Mae hyn yn trawsnewid at yr amser ganlynol ar y gweinydd: + Beth mae hyn yn golygu?]]> + Ydych chi'n sicr eich bod eisiau dileu'r eitem yma? + Mae'r priodwedd %0% yn defnyddio'r golygydd %1% sydd ddim yn cyd-fynd â Chynnwys Amnyth. + Wyt ti'n siŵr fod ti eisiau dileu pob eitem? + Nid oes unrhyw fathau o gynnwys wedi'u ffurfweddu ar gyfer yr eiddo hwn. + Ychwanegu teip elfen + Dewis teip elfen + Dewis y grŵp dylid arddangos ei briodweddau. Os caiff ei adael yn wag, bydd y grŵp cyntaf ar yr elfen yn cael ei defnyddio. + Rhowch fynegiad angular i werthuso yn erbyn pib eitem am ei enw. Defnyddiwch + i ddangos y mynegai'r eitem + Ychwanegu blwch testun arall + Dileu'r blwch testun yma + Gwraidd cynnwys + Cynnwys eitemau cynnwys heb eu cyhoeddi. + Mae'r gwerth yma'n gudd. Os ydych chi angen hawl i weld y gwerth yma, cysylltwch â gweinyddwr eich gwefan. + Mae'r gwerth yma'n gudd. + Pa ieithoedd yr hoffech chi eu cyhoeddi? Mae pob iaith sydd â chynnwys wei cael ei arbed! + Pa ieithoedd yr hoffech chi eu cyhoeddi? + Pa ieithoedd yr hoffech chi eu arbed? + Mae pob iaith sydd â chynnwys yn cael ei arbed wrth greu! + Pa ieithoedd hoffech chi anfon am gymeradwyaeth? + Pa ieithoedd yr hoffech chi eu hamserlennu? + Dewiswch yr ieithoedd i'w anghyhoeddi. Bydd anghyhoeddi iaith orfodol yn anghyhoeddi pob iaith. + Ieithoedd Cyhoeddedig + Ieithoedd heb ei gyhoeddi + Ieithoedd heb eu haddasu + Nid yw'r ieithoedd hyn wedi'u creu + Bydd pob amrywiad newydd yn cael ei arbed. + P'un amrywiadau wyt ti eisiau cyhoeddi? + Dewiswch pa amrywiadau wyt ti eisiau arbed. + Dewiswch pa amrywiadau i anfon am gymeradwyaeth. + Gosod cyhoeddi rhestredig... + Dewiswch yr amrywiadau i'w anghyhoeddi. Bydd anghyhoeddi iaith orfodol yn anghyhoeddi pob amrywiad. + Mae'r amrywiadau canlynol yn ofynnol er mwyn i gyhoeddi: + Ni ddim yn barod i Gyhoeddi + Barod i Gyhoeddi? + Barod i Arbed? + Anfonwch am gymeradwyaeth + Dewiswch y dyddiad a'r amser i gyhoeddi a / neu anghyhoeddi'r eitem gynnwys. + Creu newydd + Gludo o'r clipfwrdd + Mae'r eitem yma yn y Bin Ailgylchu + + + Creu Templed Cynnwys newydd o '%0%' + Gwag + Dewis Templed Cynnwys + Templed Cynnwys wedi'i greu + Creuwyd Templed Cynnwys o '%0%' + Mae Templed Cynnwys gyda'r un enw yn bodoli eisoes + Mae Templed Cynnwys yn gynnwys sydd wedi'i ddiffinio o flaen llaw y gellir ei ddewis gan olygwr i'w ddefnyddio fel sail ar gyfer creu cynnwys newydd + + + Cliciwch i lanlwytho + Gollyngwch eich ffeiliau yma... + Dolen i gyfrwng + neu cliciwch yma i ddewis ffeiliau + Gallwch lusgo ffeiliau yma i lanlwtho. + Dim ond mathau caniatol o ffeil sydd + Ni ellir lanlwytho'r ffeil yma, nid yw math y ffeil yn wedi'i gymeradwyo + Maint ffeil uchaf + Gwraidd gyfrwng + Methwyd symud cyfrwng + Ni all y ffolderi rhiant a chyrchfan fod yr un peth + Methwyd copïo cyfrwng + Methwyd creu ffolder o dan id rhiant %0% + Methwyd ailenwi'r ffolder gyda id %0% + Llusgo a gollwng eich ffeil(iau) i mewn i'r ardal + Ni chaniateir llwytho i fyny yn y lleoliad hwn. + + + Creu aelod newydd + Pob Aelod + Nid oes gan grwpiau aelodau unrhyw eiddo ychwanegol ar gyfer golygu. + + + Ble hoffwch greu eitem newydd %0% + Creu eitem o dan + Dewiswch y fath o ddogfen hoffwch greu templed dogfen ar ei gyfer + Rhoi enw ffolder i mewn + Dewiswch fath a theitl + Mathau o Ddogfennau o fewn y adran Gosodiadau, gan olygu y opsiwn Mathau o nod blentyn caniataol o dan Caniatadau]]> + Mathau o Ddogfennau tu fewn y adran Gosodiadau.]]> + Nid yw'r dudalen a ddewiswyd yn y goeden gynnwys yn caniatáu i unrhyw dudalennau gael eu creu oddi tani. + Golygu caniatâd ar gyfer y math hwn o ddogfen + Creu Math o Ddogfen newydd + Mathau o Ddogfennau o fewn y adran Gosodiadau, gan olygu y opsiwn Caniatáu fel gwraidd o dan Caniatadau]]> + "mathau o gyfrwng".]]> + Nid yw'r cyfryngau a ddewiswyd yn y goeden yn caniatáu i unrhyw gyfryngau eraill gael eu creu oddi tano. + Golygu caniatâd ar gyfer y math hwn o gyfryngau + Math o Ddogfen heb dempled + Ffolder newydd + Math o ddata newydd + Ffeil JavaScript newydd + Rhan-wedd wag newydd + Macro rhan-wedd newydd + Rhan-wedd newydd o damaid + Macro rhan-wedd wag newydd o damaid + Macro rhan-wedd newydd (heb macro) + Ffeil ddalen arddull newydd + Ffeil ddalen arddull Golygydd Testun Cyfoethog newydd + Macro rhan-wedd wag newydd + + + Pori eich gwefan + - Cuddio + Os nad yw Umbraco yn agor, efallai byddwch angen galluogi popups o'r safle yma + wedi agor mewn ffenestr newydd + Ailgychwyn + Ymweld â + Croeso + + + Aros + Hepgor newidiadau + Mae gennych chi newidiadau sydd heb eu achub + Ydych chi'n sicr eich bod eisiau llywio i ffwrdd o'r dudalen yma? - mae gennych chi newidiadau sydd heb eu achub + Bydd cyhoeddi yn gwneud yr eitemau a ddewiswyd yn weladwy ar y wefan. + Bydd anghyhoeddi yn tynnu'r eitemau a ddewiswyd a'u holl ddisgynyddion o'r safle. + Bydd dadgyhoeddi yn dileu'r dudalen yma a phob un o'i phlant o'r safle. + Mae gennych chi newidiadau heb eu cadw. Bydd gwneud newidiadau i'r Math o Ddogfen yn taflu'r newidiadau i ffwrdd. + + + Wedi gwneud + Wedi dileu eitem %0% + Wedi dileu %0% eitem + Wedi dileu %0% allan o %1% eitem + Wedi dileu %0% allan o %1% o eitemau + Wedi cyhoeddi eitem %0% + Wedi cyhoeddi %0% o eitemau + Wedi cyhoeddi %0% allan o %1% eitem + Wedi cyhoeddi %0% allan o %1% eitemau + Wedi dadgyhoeddi eitem %0% + Wedi dadgyhoeddi %0% o eitemau + Wedi dadgyhoeddi %0% allan o %1% eitem + Wedi dadgyhoeddi %0% allan o %1% o eitemau + Wedi symud eitem %0% + Wedi symud %0% o eitemau + Wedi symud %0% allan o %1% eitem + Wedi symud %0% allan o %1% o eitemau + Wedi copïo eitem %0% + Wedi copïo %0% o eitemau + Wedi copïo %0% allan o %1% eitem + Wedi copïo %0% allan o %1% eitemau + + + Teitl y ddolen + Dolen + Angor / llinyn ymholi + Enw + Gweinyddu enwau gwesteia + Cau'r ffenestr yma + Ydych chi'n sicr eich bod eisiau dileu + Wyt ti'n siŵr fod ti eisiau dileu %0% yn seiliedig ar %1% + + Ydych chi'n sicr eich bod eisiau analluogi + + Wyt ti'n siŵr fod ti eisiau dileu + %0%]]> + %0%]]> + + Ydych chi'n sicr? + Ydych chi'n sicr? + Torri + Golygu Eitem Geiriadur + Golygu Iaith + Golygu cyfrwng a dewiswyd + Mewnosod dolen leol + Mewnosod nod + Mewnosod pennawd graffig + Mewnosod llun + Mewnosod dolen + Cliciwch i ychwanegu Macro + Mewnosod tabl + Bydd hyn yn dileu'r iaith + Gall newid y diwylliant ar gyfer iaith fod yn weithrediad drud a bydd yn arwain at ailadeiladu'r storfa cynnwys a'r mynegeion + Golygwyd ddiwethaf + Dolen + Dolen fewnol: + Wrth ddefnyddio dolenni leol, defnyddiwch "#" o flaen y ddolen + Agor mewn ffenestr newydd? + Gosodiadau Macro + Nid yw'r macro yma yn cynnwys unrhyw briodweddau gallwch chi olygu + Gludo + Golygu hawliau ar gyfer + Gosod hawliau ar gyfer + Gosod hawliau ar gyfer %0% ar gyfer y grŵp defnyddwyr %1% + Dewiswch y grŵpiau defnyddwyr yr ydych eisiau gosod hwaliau ar eu cyfer + Mae'r eitemau yn y bin ailgylchu yn cael eu dileu. Peidiwch â chau'r ffenestr yma wrth i'r gweithrediad gymryd lle + Mae'r bin ailgylchu yn awr yn wag + Pan gaiff eitemau eu dileu o'r bin ailgylchu, byddent yn diflannu am byth + regexlib.com ar hyn o bryd, nid oes gennym reolaeth dros hyn. Mae'n ddrwg iawn gennym ni am yr anghyfleustra.]]> + Chwiliwch am fynegiad cyson er mwyn ychwanegu dilysiad i faes ffurflen. Enghraifft: 'email, 'zip-code' 'url' + Dileu Macro + Maes Gofynnol + Safle wedi'i ail-fynegi + Mae storfa'r wefan wedi'i ddiweddaru. Mae holl gynnwys cyhoeddi wedi'i ddiweddaru, ac mae'r holl gynnwys sydd heb ei gyhoeddi yn dal i fod heb ei gyhoeddi + Bydd storfa'r wefan yn cael ei adnewyddu. Bydd holl gynnwys cyhoeddi yn cael ei ddiweddaru, ac bydd holl gynnwys sydd heb ei gyhoeddi yn dal i fod heb ei gyhoeddi. + Nifer o golofnau + Nifer o resi + + Gosodwch id dalfan wrth osod ID ar eich dalfan gallwch chwistrellu cynnwys i mewn i'r templed yma o dempledi blentyn, + wrth gyfeirio at yr ID yma gan ddefnyddio elfen <asp:content />.]]> + + + Dewiswch id dalfan o'r rhestr isod. Gallwch ddim ond + ddewis Id (neu sawl) o feistr y dempled bresennol.]]> + + Cliciwch ar y llun i weld y maint llawn + Dewis eitem + Gweld Eitem Storfa + Creu ffolder... + Perthnasu at y gwreiddiol + Cynnwys disgynyddion + Y gymuned fwyaf cyfeillgar + Dolen i dudalen + Agor y ddolen ddogfen mewn ffenestr neu tab newydd + Dolen i gyfrwng + Dolen i ffeil + Dewis nod cychwyn cynnwys + Dewis cyfrwng + Dewis y math o gyfrwng + Dewis eicon + Dewis eitem + Dewis dolen + Dewis macro + Dewis cynnwys + Dewiswch y math o gynnwys + Dewis nod cychwyn cyfrwng + Dewis aelod + Dewis grŵp aelod + Dewiswch fath aelod + Dewis nod + Dewis adran + Dewis defnyddiwr + Dewis defnyddwyr + Dim eiconau wedi'u darganfod + Does dim paramedrau ar gyfer y macro yma + Does dim macro ar gael i fewnosod + Darparwyr mewngofnodi allanol + Manylion Eithriad + Trywydd stac + Eithriad Fewnol + Dolenni eich + Dad-ddolenni eich + cyfrif + Dewiswch olygwr + Dewiswch ffurfweddiad + Dewiswch damaid + Bydd hyn yn dileu'r nod a'i holl ieithoedd. Os mai dim ond un iaith yr ydych am ei dileu, ewch i'w anghyhoedd yn lle. + %0%.]]> + %0% o'r grŵp %1%]]> + Ydw, dileu + + + Nid oes unrhyw eitemau geiriadur. + + + + %0%' islaw
Gallwch ychwanegu ieithoedd ychwanegol o dan 'ieithoedd' yn y ddewislen ar y chwith + ]]> +
+ Enw Diwylliant + Golygu allwedd yr eitem geiriadur. + + + + Trosolwg Geiriadur + + + Chwilwyr wedi'u Ffurfweddu + Yn dangos priodweddau ac offer ar gyfer unrhyw Chwiliwr wedi'i ffurfweddu (h.y. fel chwiliwr aml-fynegai) + Gwerthoedd maes + Statws iechyd + Statws iechyd y mynegai ac os gellir ei ddarllen + Mynegewyr + Gwybodaeth mynegai + Yn rhestru priodweddau'r mynegai + Rheoli mynegeion Examine + Yn caniatáu ichi weld manylion pob mynegai ac yn darparu rhai offer ar gyfer rheoli'r mynegeion + Ailadeiladu mynegai + + + Yn dibynnu ar faint o gynnwys sydd yn eich gwefan, gallai hyn gymryd cryn amser.
+ Ni argymhellir ailadeiladu mynegai ar adegau o draffig gwefan uchel neu pan fydd golygyddion yn golygu cynnwys. + ]]> +
+ Chwilwyr + Chwiliwch y mynegai a gweld y canlyniadau + Offer + Offer i reoli'r mynegai + meysydd + Ni ellir darllen yr mynegai a bydd angen ei ailadeiladu + Mae'r broses yn cymryd mwy o amser na'r disgwyl, gwiriwch y log Umbraco i weld os mae wedi bod unrhyw wall yn ystod y gweithrediad hwn + Ni ellir ailadeiladu'r mynegai hwn oherwydd nad yw wedi'i aseinio + IIndexPopulator + + + Darparwch eich enw defnyddiwr + Darparwch eich cyfrinair + Cadarnhewch eich cyfrinair + Enwch y %0%... + Darparwch enw... + Darparwch ebost... + Darparwch enw defnyddiwr... + Label... + Darparwch ddisgrifiad... + Teipiwch i chwilio... + Teipiwch i hidlo... + Teipiwch i ychwanegu tagiau (gwasgwch enter ar ôl pob tag)... + Darparwch eich ebost + Darparwch neges... + Mae eich enw defnyddiwr fel arfer eich cyfeiriad ebost + #gwerth neu ?allwedd=gwerth + Darparwch enw arall... + Yn generadu enw arall... + Creu eitem + Creu + Golygu + Enw + + + Caniatáu ar y gwraidd + Dim ond Mathau o Gynnwys gyda hwn wedi ticio all gael eu creu ar lefel wraidd coed Cynnwys a Chyfrwng + Mathau o nod blentyn caniataol + Cyfansoddiadau Mathau o Ddogfen + Creu + Dileu tab + Disgrifiad + Tab newydd + Tab + Ciplun bach + Galluogi gwedd rhestr + Ffurfweddu'r eitem gynnwysi ddangos rhestr trefnadwy & a chwiladwy o'i phlant, ni fydd y plant yn cael eu dangos yn y goeden + Gwedd rhestr bresennol + Y fath o ddata gwedd rhestr gweithredol + Creu gwedd rhestr pwrpasol + Dileu gwedd rhestr pwrpasol + Mae math o gynnwys, math o gyfrwng neu math o aeold gyda'r enw arall yma'n bodoli eisoes + + + Wedi ailenwi + Darparwch enw ffolder newydd yma + %0% wedi ailenwi i %1% + + + Ychwanegu cyn-werth + Math o ddata cronfa ddata + GUID golygydd priodwedd + Golygydd priodwedd + Botymau + Galluogi gosodiadau datblygedig ar gyfer + Galluogi dewislen cyd-destun + Maint fwyaf diofyn llun wedi'i fewnosod + Taflenni arddull perthnasol + Dangos label + Lled ac uchder + Holl fathau o briodweddau & data priodwedd + yn defnyddio'r fath yma o ddata yn cael eu dileu yn barhaol, cadarnhewch eich bod eisiau dileu'r rhain hefyd + Iawn, dileu + a holl fathau o briodwedd & data priodwedd sy'n defnyddio'r math o ddata yma + Dewiswch y ffolder i symud + i'r strwythyr goeden isod + wedi symud o dan + + %0% yn dileu'r briodweddau a'i data o'r eitemau canlynol]]> + Rwy'n deall y weithred hon yn dileu'r holl briodweddau a data sy'n seiliedig ar Fath o Ddata hon + + + Mae eich data wedi'i achub, ond cyn i chi allu cyhoeddi'r dudalen yma, mae yna wallau yr ydych angen eu gwirio yn gyntaf: + Nid yw'r darparwr aeoldaeth bresennol yn cefnogi newid cyfrinair (EnablePasswordRetrieval angen cael ei osod i true) + %0% yn bodoli eisoes + Roedd yna wallau: + Roedd yna wallau: + Dylai'r cyfrinair fod o leiaf %0% nod o hyd a chynnwys o leiaf %1% nod(au) sydd ddim yn llythyren na rhif + %0% angen bod yn gyfanrif + Mae'r maes %0% yn y tab %1% yn ofynnol + %0% yn faes ofynnol + %0% yn %1% mewn fformat annilys + %0% mewn fformat annilys + + + Derbynwyd gwall o'r gweinydd + Mae'r math o ffeil yma wedi'i wahardd gan y gweinyddwr + NODYN! Er bod CodeMirror wedi'i alluogi gan y ffurfwedd, mae o wedi'i analluogi mewn Internet Explorer gan nad yw'n ddigon cadarn. + Darparwch yr enw ac yr enw arall ar y math o briodwedd newydd! + Mae yna broblem gyda hawliau darllen/ysgrifennu i ffeil neu ffolder penodol + Gwall yn llwytho sgript Rhan-Wedd (ffeil: %0%) + Gwall yn llwytho Rheolydd Defnyddiwr '%0%' + Gwall yn llwytho Rheolydd Pwrpasol (Gwasaneth: %0%, Math: '%1%') + Gwall yn llwytho sgript MacroEngine (ffeil: %0%) + "Gwall yn dosbarthu'r ffeil XSLT: %0% + "Gwall yn darllen y ffeil XSLT: %0% + Darparwch deitl + Dewiswch fath + Rydych ar fîn gwneud y llun yn fwy 'na'r maint gwreiddiol. Ydych chi'n sicr eich bod eisiau parhau? + Gwall yn y sgript python + Nid yw'r sgript python wedi'i achub gan ei fod yn cynnwys gwall(au) + Nod gychwynnol wedi'i ddileu, cysylltwch â'ch gweinyddwr + Marciwch gynnwys cyn newid arddull + Dim arddulliau gweithredol ar gael + Symudwch y cyrchwr ar ochr chwith y ddwy gell yr ydych eisiau cyfuno + Ni allwch hollti cell sydd heb ei gyfuno. + Mae gan briodwedd hon gwallau + Gwall yn y ffynhonnell XSLT + Nid yw'r XSLTwedi'i achub gan ei fod yn cynnwys gwall(au) + Mae gwall ffurfwedd gyda'r math o ddata sy'n cael ei ddefnyddio ar gyfer y priodwedd yma, gwiriwch y fath o ddata + + + Dewisiadau + Amdano + Gweithred + Gweithredoedd + Ychwanegu + Enw arall + Holl + Ydych chi'n sicr? + Yn ôl + Yn ôl i'r trosolwg + Ffin + wrth + Canslo + Ymyl cell + Dewis + Clirio + Cau + Cau Ffenest + Sylw + Cadarnhau + Gorfodi + Gorfodi cyfraneddau + Cynnwys + Bwrw ymlaen + Copïo + Creu + Adran tocio + Cronfa ddata + Dyddiad + Diofyn + Dileu + Wedi dileu + Yn dileu... + Cynllun + Geiriadur + Dimensiynau + Gwaredu + I lawr + Lawrlwytho + Golygu + Golygwyd + Elfennau + Ebost + Gwall + Maes + Canfod + Cyntaf + Canolbwynt + Cyffredinol + Grŵpiau + Grŵp + Uchder + Help + Cuddio + Hanes + Eicon + Id + Mewnforio + Cynnwys is-ffolderi wrth chwilio + Search only this folder Chwilio yn ffolder hwn yn unig + Gwybodaeth + Ymyl mewnol + Mewnosod + Gosod + Annilys + Unioni + Label + Iaith + Olaf + Gosodiad + Dolenni + Yn llwytho + Wedi cloi + Mewngofnodi + Allgofnodi + Allgofnodi + Macro + Gofynnol + Neges + Symud + Mwy + Enw + Newydd + Nesaf + Na + o + I ffwrdd + Iawn + Agor + Opsiynau + Ymlaen + neu + Trefnu wrth + Cyfrinair + Llwybr + ID Dalfan + Un eiliad os gwelwch yn dda... + Blaenorol + Priodweddau + Ailadeiladu + Ebost i dderbyn data ffurflen + Bin Ailgylchu + Mae eich bin ailgylchu yn wag + Ail-lwytho + Ar ôl + Dileu + Ailenwi + Adnewyddu + Gofynnol + Adfer + Ceisio eto + Hawliau + Cyhoeddi ar amserlen + Chwilio + Mae'n ddrwg gennym ni, ni all ganfod beth roeddwch chi'n chwilio amdano. + Dim eitemau wedi'u hychwanegu + Gweinydd + Gosodiadau + Dangos + Dangos y dudlaen wrth Anfon + Maint + Trefnu + Statws + Cyflwyno + Llwyddiant + Math + Math i chwilio... + o dan + I fyny + Diweddaru + Uwchraddio + Lanlwytho + Url + Defnyddiwr + Enw defnyddiwr + Gwerth + Gwedd + Croeso... + Lled + Ie + Ffolder + Canlyniadau chwilio + Ail-drefnu + Rydw i wedi gorffen ail-drefnu + Rhagolwg + Newid cyfrinair + i + Gwedd rhestr + Yn achub... + presennol + Mewnblannu + Adfer + dewiswyd + Arall + Erthyglau + Fideos + Clirio + Arsefydlu + + + Du + Gwyrdd + Melyn + Oren + Glas + Llwyd Las + Llwyd + Brown + Glas Golau + Gwyrddlas + Gwyrdd Golau + Leim + Melyngoch + Oren Ddwfn + Coch + Pinc + Piws + Piws Ddwfn + Dulas + + + Ychwanegu tab + Ychwanegu grŵp + Ychwanegu priodwedd + Ychwanegu golygydd + Ychwanegu templed + Ychwanegu nod blentyn + Ychwanegu plentyn + Golygu math o ddata + Llywio adrannau + Llwybrau byr + dangos llwybrau byr + Toglo gwedd rhestr + Toglo caniatáu fel gwraidd + Sylwi/Dad-sylwi llinellau + Dileu llinell + Copïo llinellau i fyny + Copïo llinellau i lawr + Symud Llinellau I Fyny + Symud Llinellau I Lawr + Cyffredinol + Golygydd + Toglo caniatáu amrywiadau diwylliant + Toglo caniatáu segmentiad + + + Lliw cefndir + Trwm + Lliw ffont + Ffont + Testun + + + Tudalen + + + Ni all y gosodydd gysylltu â'r gronfa ddata. + Methwyd achub y ffeil web.config. Ceisiwch newid y llinyn gyswllt yn uniongyrchol. + Canfwyd eich cronfa ddata ac mae'n cael ei adnabod fel + Ffurfwedd gronfa ddata + + gosod i osod y gronfa ddata %0% Umbraco + ]]> + + Nesaf i fwrw ymlaen.]]> + + Cronfa ddata heb ei ganfod! Gwiriwch fod y gwybodaeth yn y "llinyn gyswllt" o'r ffeil "web.config" yn gywir.

+

Er mwyn parhau, newidiwch y ffeil "web.config" (gan ddefnyddio Visual Studio neu eich hoff olygydd testun), rholiwch at y gwaelod, ychwanegwch y llinyn gyswllt ar gyfer eich cronfa ddata yn yr allwedd o'r enw "UmbracoDbDSN" ac achub y ffeil.

+

+ Cliciwch y botwm ceisio eto pan rydych wedi + gorffen.
+ Mwy o wybodaeth am newid y ffeil web.config yma.

]]> +
+ + + Cysylltwch â'ch darparwr gwe (ISP) os oes angen. + Os ydych chi'n gosod ar beiriant leol neu weinydd, efallai bydd angen gwybodaeth o'ch gweinyddwr system arnoch.]]> + + + + Gwasgwch y botwm uwchraddio i uwchraddio eoch gronfa ddata i Umbraco %0%

+

+ Peidiwch â phoeni - ni fydd unrhyw gynnwys yn cael ei ddileu a bydd popeth yn parhau i weithio wedyn! +

+ ]]> +
+ + Gwasgwch Nesaf i + barhau. ]]> + + nesaf i barhau gyda'r dewin ffurfwedd]]> + Mae angen newid cyfrinair y defnyddiwr Diofyn!]]> + Mae'r defnyddiwr Diofyn wedi'u analluogi neu does dim hawliau i Umbraco!

Does dim angen unrhyw weithredoedd pellach. Cliciwch Nesaf i barhau.]]> + Mae cyfrinair y defnyddiwr Diofyn wedi'i newid yn llwyddiannus ers y gosodiad!

Does dim angen unrhyw weithredoedd pellach. Cliciwch Nesaf i barhau.]]> + Mae'r cyfrinair wedi'i newid! + Cewch gychwyn gwych, gwyliwch ein fideos rhaglith + Wrth glicio'r botwm nesaf (neu newid y umbracoConfigurationStatus yn web.config), rydych yn derbyn y trwydded ar gyfer y meddalwedd yma fel y nodir yn y blwch isod. Sylwch fod y dosbarthiad Umbraco yma yn cynnwys 2 drwydded gwahanol, y trwydded cod agored MIT ar gyfer y fframwaith ac y trwydded Umbraco rhadwedd sy'n ymdrin â'r Rhyngwyneb Defnyddiwr. + Heb osod eto. + Ffeiliau a ffolderi wedi'u effeithio + Mwy o wybodaeth am osod hawliau ar gyfer Umbraco yma + Rydych angen caniatáu i ASP.NET newid hawliau ar y ffeiliau/ffolderi canlynol + + Mae eich gosodiadau hawliau bron a bod yn berffaith!

+ Gallwch redeg Umbraco heb broblemau, ond ni fydd yn bosibl i chi osod pecynnau sydd wedi'u hargymell er mwyn cymryd mantais llawn o Umbraco.]]> +
+ Sut i Gywiro + Cliciwch yma i ddarllen y ferswin destun + fideo tiwtorial ar osod hawliau ffolder ar gyfer Umbraco neu darllenwch y fersiwn destun.]]> + + Gall eich gosodiadau hawliau fod yn broblem! +

+ Gallwch redeg Umbraco heb broblemau, ond ni fydd yn bosibl i chi greu ffolderi neu gosod pecynnau sydd wedi'u hargymell er mwyn cymryd mantais llawn o Umbraco.]]> +
+ + Nid yw eich gosodiadau hawliau yn barod ar gyfer Umbraco! +

+ Er mwyn rhedeg Umbraco, bydd angen i chi ddiweddaru eich gosodiadau hawliau.]]> +
+ + Mae eich gosodiadau hawliau yn berffaith!

+ Rydych yn barod i redeg Umbraco a gosod pecynnau!]]> +
+ Yn datrys y broblem ffolder + Dilynwch y ddolen yma ar gyfer mwy o wybodaethar broblemau gyda ASP.NET a chreu ffolderi + Gosod hawliau ffolderi + + + + Rydw i eisiau ail-gychwyn + + dysgwch sut) + Gallwch ddewis i osod Runway yn hwyrach os hoffwch chi. Ewch at yr adran Datblygwr a dewiswch Pecynnau. + ]]> + + Rydych newydd osod platfform glân Umbraco. Beth hoffwch chi wneud nesaf? + Mae Runway wedi'i osod + + + Dyma ein rhestr o fodylau yr ydym yn argymell, ticiwch y rhai hoffwch chi'u gosod, neu gwelwch yr holl restr o fodylau + ]]> + + Dim ond wedi'i argymell ar gyfer defnyddwyr brofiadol + Hoffwn i gychwyn gyda gwefan syml + + + Mae "Runway" yn wefan syml sy'n darparu mathau o ddogfennau a thempledi syml. Gall y gosodwr osod Runway i chi yn awtomatig, + ond gallwch olygu, estyn neu ei ddileu yn hawdd. Nid yw'n angenrheidiol a gallwch ddefnyddio Umbraco yn berffaith heb. Ond, + mae Runwayyn cynnig sylfaen hawdd wedi'i seilio ar arferion gorau er mwyn i chi gychwyn yn gyflymach nag erioed. + Os rydych chi'n dewis gosod Runway, gallwch ddewis blociau adeiliadu syml o'r enw Modylau Runway er mwyn mwyhau eich tudalennau Runway. +

+ + Wedi cynnwys gyda Runway: Tudalen Hafan, Tudalen Cychwyn Allan, Tudalen Gosod Modylau.
+ Modylau Dewisol: Llywio Dop, Map o'r wefan, Cysylltu, Oriel. +
+ ]]> +
+ Beth yw Runway + Cam 1/5 Derbyn trwydded + Cam 2/5: Ffurfwedd Gronfa Ddata + Cam 3/5: Dilysu Hawliau Ffeiliau + Cam 4/5: Gwirio Diogelwch Umbraco + Cam 5/5: Mae Umbraco yn barod i chi gychwyn + Diolch am ddewis Umbraco + + Porwch eich safle newydd +Rydych wedi gosod Runway, felly beth am weld sut mae eich gwefan newydd yn edrych.]]> + + + Cymorth a gwyboaeth bellach +Cewch gymorth o'n cymuned gwobrwyol, porwch drwy ein dogfennaeth neu gwyliwch fideos yn rhad ac am ddim ar sut i adeiladu gwefan syml, sut i ddefnyddio pecynnau a chanllaw cyflym i dermeg Umbraco]]> + + Mae Umbraco wedi'i osod %0% ac mae'n barod i'w ddefnyddio + + /web.config a diweddaru'r allwedd AppSetting UmbracoConfigurationStatus yng ngwaelod y gwerth o '%0%'.]]> + + + yn syth wrth glicio ar y botwm "Cychwyn Umbraco" isod.
Os ydych yn newydd i Umbraco, +gallwch ddarganfod digonedd o adnoddau ar ein tudalennau cychwyn allan.]]> +
+ + Cychwyn Umbraco +Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwangeu cynnwys, diweddaru'r templedi a thaflenni arddull neu ychwanegu nodweddion newydd]]> + + Methwyd cysylltu â'r gronfa ddata. + Umbraco Fersiwn 3 + Umbraco Fersiwn 4 + Gwylio + + Umbraco %0% ar gyfer gosodiad ffres neu uwchraddio o ferswin 3.0. +

+ Gwasgwch "nesaf" i gychwyn y dewin.]]> +
+ + + Côd Diwylliant + Enw Diwylliant + + + Rydych wedi segura a bydd allgofnodi awtomatig yn digwydd mewn + Adnewyddwch rwan er mwyn achub eich gwaith + + + Dydd Sul Swmpus + Dydd Llun Llwyddiannus + Dydd Mawrth Moethus + Dydd Mercher Melys + Dydd Iau Iachus + Dydd Gwener Gwych + Dydd Sadwrn Syfrdannus + Mewngofnodwch isod + Mewngofnodwch gyda + Sesiwn wedi cyrraedd terfyn amser + © 2001 - %0%
Umbraco.com

]]>
+ Wedi anghofio eich cyfrinair? + Bydd ebost yn cael ei anfon i'r cyfeiriad darparwyd gyda dolen i ailosod eich cyfrinair + Bydd ebost gyda chyfarwyddiadau ailosod cyfrinair yn cael ei anfon at y cyfeiriad darparwyd os yw'n cyfateb â'n cofnodion + Dangos cyfrinair + Cuddio cyfrinair + Dychwelyd i'r ffurflen mewngofnodi + Darparwch gyfrinair newydd + Mae eich cyfrinair wedi'i ddiweddaru + Mae'r ddolen rydych wedi clicio arno naill ai yn annilys neu wedi dod i ben + Umbraco: Ailosod Cyfrinair + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Ailosod cyfrinair wedi dymuno +

+

+ Eich enw defnyddiwr ar gyfer swyddfa gefn Umbraco yw: %0% +

+

+ + + + + + +
+ + Cliciwch y ddolen yma er mwyn ailosod eich cyfrinair + +
+

+

Os na allwch glicio ar y ddolen yma, copïwch a gludwch y URL i mewn i'ch porwr:

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

+
+
+


+
+
+ + + ]]> +
+ + + Dashfwrdd + Adrannau + Cynnwys + + + Dewis tudalen uwchben... + %0% wedi copïo i %1% + Dewiswch ble ddylai'r ddogfen %0% gael ei gopïo i isod + %0% wedi ei symud i %1% + Dewiswch ble ddylai'r ddogfen %0% gael ei symud i isod + wedi ei ddewis fel gwraidd eich cynnwys newydd, cliciwch 'iawn' isod. + Dim nod wedi'i ddewis eto, dewiswch nod yn y rhestr uchod yn gyntaf cyn clicio 'iawn' + Nid yw'r nod bresennol yn cael ei ganiatáu o dan y nod ddewiswyd oherwydd ei fath + Ni all y nod bresennol gael ei symud i un o'i is-dudalennau + Ni all y nod bresennol fodoli ar y gwraidd + Nid yw'r gweithred wedi'i ganiatáu gan nad oes gennych ddigon o hawliau ar gyfer 1 neu fwy o ddogfennau blentyn. + Perthnasu eitemau wedi'u copïo at y rhai gwreiddiol + + + %0%]]> + Gosodiad hysbysiadau wedi cadw am + + + + Mae'r ieithoedd canlynol wedi'u haddasu %0% + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Helo %0%, +

+

+ Mae hyn yn ebost awtomatig i'ch hysbysu fod y dasg '%1%' wedi'i berfformio ar y dudalen '%2%' gan y defnyddiwr '%3%' +

+ + + + + + +
+ +
+ GOLYGU
+
+

+

Crynodeb diweddariad:

+ + %6% +
+

+

+ Mwynhewch eich diwrnod!

+ Hwyl fawr oddi wrth y robot Umbraco +

+
+
+


+
+
+ + + ]]> +
+ + Mae'r ieithoedd canlynol wedi'u haddasu:

+ %0% + ]]> +
+ [%0%] Hysbysiad am %1% wedi perfformio am %2% + Hysbysiadau + + + Gweithredoedd + Creu + Creu pecyn + + + Pori a darganfod y pecyn. Fel arfer, mae gen becynnau Umbraco estyniadau ".umb" neu ".zip". + ]]> + + Bydd hwn yn dileu'r pecyn + Gollwng i lanlwytho + Cynhwyswch yr holl nodau plentyn + neu cliciwch yma i ddewis ffeil pecyn + Lanlwytho pecyn + Gosod pecyn leol wrth ddewis o'ch peiriant. Dylwch ddim ond osod pecynnau o ffynonellau yr ydych yn adnabod a bod gennych hyder ynddynt + Lanlwytho pecyn arall + Canslo a lanlwytho pecyn arall + Trwydded + Rydw i'n derbyn + termau defnydd + Llwybr i'r ffeil + Llwybr llwyr i'r ffeil (ie: /bin/umbraco.bin) + Wedi'i osod + Gosod yn lleol + Gosod pecyn + Gorffen + Pecynnau wedi'u gosod + Nid oes gennych unrhyw becynnau wedi'u gosod + 'Pecynnau' yng nghornel dop, dde eich sgrîn]]> + Nid oes gan y pecyn hwn unrhyw olwg cyfluniad + Nid oes unrhyw becynnau wedi'u creu eto + Camau Gweithredu Pecyn + URL y Awdur + Cynnwys y Pecyn + Ffeiliau y Pecyn + URL Eicon + Gosod pecyn + Trwydded + URL Trwydded + Priodweddau Pecyn + Chwilio am becynnau + Canlyniadau ar gyfer + Ni allwn ddarganfod unrhyw beth ar gyfer + Ceisiwch chwilio am becyn arall neu porwch drwy'r categorïau + Poblogaidd + Pecynnau newydd + yn cynnwys + pwyntiau karma + Gwybodaeth + Perchennog + Cyfranwyr + Creuwyd + Fersiwn bresennol + Fersiwn .NET + Lawrlwythiadau + Hoffi + Cydweddoldeb + Mae'r pecyn yma yn gydnaws â'r fersiynau canlynol o Umbraco, fel y mae aelodau'r gymued yn adrodd yn ôl. Ni all warantu cydweddoldeb cyflawn ar gyfer fersiynau sydd wedi'u hadrodd o dan 100% + Ffynonellau allanol + Awdur + Arddangosiad + Dogfennaeth + Meta ddata pecynnau + Enw pecyn + Does dim eitemau o fewn y pecyn + +
+ Gallwch ddileu hyn yn ddiogel o'r system wrth glicio "dadosod pecyn" isod.]]> +
+ Dim uwchraddiadau ar gael + Dewisiadau pecyn + Readme pecyn + Ystorfa pecyn + Cadarnhau dadosod pecyn + Pecyn wedi dadosod + Cafodd y pecyn ei ddadosod yn llwyddiannus + Dadosod pecyn + + + Rhybudd: bydd unrhyw ddogfennau, cyfrwng ayyb sy'n dibynnu ar yr eitemau yr ydych am ddileu yn torri, a gall arwain at system ansefydlog, + felly dadosodwch gyda gofal. Os oes unrhyw amheuaeth, cysylltwch ag awdur y pecyn.]]> + + Lawrlwytho diweddariad o'r ystorfa + Uwchraddio pecyn + Cyfarwyddiadau uwchraddio + Mae yna uwchraddiad ar gael ar gyfer y pecyn yma. Gallwch lawrlwytho'n uniongyrchol o'r ystorfa pecynnau Umbraco. + Fersiwn pecyn + Uwchraddio o ferswin + Hanes ferswin pecyn + Gweld gwefan pecyn + Pecyn wedi'i osod eisoes + Ni all y pecyn yma gael ei osod, mae angen fersiwn Umrbaco o leiaf + Dadosod... + Lawrlwytho... + Mewnforio... + Gosod... + Ailgychwyn, arhoswch... + Wedi cwblhau, bydd eich porwr yn adnewyddu, arhoswch... + Cliciwch 'Cwblhau' i orffen y gosodiad ac adnewyddu'r dudalen. + Lanlwytho pecyn... + + + Gludo gyda fformatio llawn (Heb ei argymell) + Mae'r testun yr ydych yn ceisio gludo yn cynnwys nodauneu fformatio arbennig. Gall hyn gael ei achosi gan ludo testun o Microsoft Word. Gall Umbraco ddileu nodau neu fformatio arbennig yn awtomatig, fel bod y cynnwys sy'n cael ei ludo yn fwy addas ar gyfer y we. + Gludo fel testun crai heb unrhyw fformatio + Gludo, ond dileu fformatio (Wedi'i hargymell) + + + Amddiffyniad yn seiliedig grŵp + Os ydych chi am ganiatáu mynediad i bob aelod o grwpiau aelodau penodol + Mae angen i chi greu grŵp aelod cyn y gallwch ddefnyddio dilysiad grŵp + Amddiffyn ar sail rôl + Os hoffwch reoli cyrchiad i'r dudalen wrth ddefnyddio dilysu ar sail rôl, gan ddefnyddio grwpiau aelodaeth Umbraco. + Mae angen i chi greu grŵp aeloadeth cyn i chi allu defnyddio dilysu ar sail rôl + Tudalen Wall + Wedi'i ddefnyddio pan mae defnyddwyr wedi mewngofnodi, ond nid oes ganddynt hawliau + Dewiswch sut i gyfyngu hawliau at y dudalen yma + %0% wedi amddiffyn rwan + Amddiffyniad wedi dileu o %0% + Tudalen Mewngofnodi + Dewiswch y dudalen sy'n cynnwys y ffurflen mewngofnodi + Dileu Amddiffyniad + Dewiswch y tudalennau sy'n cynnwys ffurflenni mewngofnodi a negeseuon gwall + Dewiswch y rolau sydd a hawliau i'r dudlaen yma + Gosodwch yr enw defnyddiwr a chyfrinair ar gyfer y dudalen yma + Amddiffyniad defnyddiwr unigol + Os hoffwch osod amddifyniad syml wrth ddefnyddio enw defnyddiwr a chyfrinair sengl + %0%?]]> + %0%]]> + %0%]]> + Amddiffyn aelodau penodol + Os ydych am ganiatáu mynediad i aelodau penodol + + + Caniatâd annigonol gan ddefnyddwyr i gyhoeddi'r holl ddogfennau disgynyddion + + + + + + + + + + + + + + + + + + + + Methodd y dilysiad ar gyfer yr iaith ofynnol '%0%'. Roedd yr iaith wedi cael ei arbed ond nid ei chyhoeddi. + Cynnwys is-dudalennau heb eu cyhoeddi + Cyhoeddi ar waith - arhoswch... + %0% allan o %1% o dudalennau wedi eu cyhoeddi... + %0% wedi ei gyhoeddi + %0% ac eu is-dudalennau wedi'u cyhoeddi + Cyhoeddi %0% ac ei holl is-dudalennau + + Cyhoeddi er mwyn cyhoeddi %0% a felly yn gwneud i'r cynnwys berthnasol fod ar gael i'r cyhoedd.

+ Gallwch gyhoeddi'r dudalen yma ac ei holl is-dudalennau wrth dicio Cynnwys tudalennau heb eu cyhoeddi isod. + ]]> +
+ + + Nid ydych chi wedi ffurfweddu unrhyw liwiau sydd wedi'u cymeradwyo + + + Gallwch ond ddewis eitemau o'r math(au): %0% + Rydych wedi dewis eitem gynnwys sydd naill ai wedi'i ddileu neu yn y bin ailgylchu + Rydych wedi dewis eitemau gynnwys sydd naill ai wedi'u dileu neu yn y bin ailgylchu + + + Rydych wedi dewis eitem gyfrwng sydd naill ai wedi'i ddileu neu yn y bin ailgylchu + Rydych wedi dewis eitemau gyfrwng sydd naill ai wedi'u dileu neu yn y bin ailgylchu + Eitem wedi'i ddileu + Yn sbwriel + + + Darparwch ddolen allanol + Dewiswch dudalen fewnol + Capsiwn + Dolen + Agor mewn ffenestr newydd + Darparwch y capsiwn arddangos + Darparwch y ddolen + + + Ailosod tocio + Achub tocio + Ychwanegu tocio newydd + Wedi gwneud + Dadwneud golygion + Diffiniad defnyddiwr + + + Dewis fersiwn i gymharu efo fersiwn bresennol + Newidiadau + Creuwyd + Fersiwn bresennol + Ni fydd testun coch yn cael ei ddangos yn y fersiwn dewiswyd. , mae gwyrdd yn golygu wedi'i ychwanegu]]> + Dogfen wedi'i rolio yn ôl + Mae hyn yn dangos y fersiwn dewiswyd ar ffurf HTML, os hoffwch weld y gwahaniaeth rhwng 2 fersiwn ar yr un pryd, defnyddiwch y wedd gwahaniaethol + Rolio yn ôl at + Dewis fersiwn + Gwedd + + + Golygu ffeil sgript + + + Gwas + Cynnwys + Tywyswr + Datblygwr + Ffurflenni + Cymorth + Dewin Ffurfweddu Umbraco + Cyfrwng + Aelodau + Cylchlythyrau + Pecynnau + Gosodiadau + Ystadegau + Cyfieithiad + Defnyddwyr + Dadansoddeg + + + ewch i + Pynciau cymorth ar gyfer + Penodau fideo ar gyfer + Teithiau + Y fideos tiwtorial Umbraco gorau + Ymweld â our.umbraco.com + Ymweld â umbraco.tv + + + Templed diofyn + Allwedd Geiriadur + Er mwyn mewnforio math o ddogfen, darganfyddwch y ffeil ".udt" ar ecih cyfrifiadur wrth glicio ar y botwn "Pori" a cliciwch "Mewnforio" (byddwch yn cael eich gofyn i gadarnhau ar y sgrîn nesaf) + Teitl Tab Newydd + Math o nod + Math + Taflen arddull + Sgript + Priodwedd taflen arddull + Tab + Teitl Tab + Tabiau + Math o Gynnwys Meistr wedi'i alluogi + Mae'r Math o Gynnwys yma yn defnyddio + Dim priodweddau wedi'u diffinio ar y tab yma. Cliciwch ar y ddolen "ychwanegu priodwedd newydd" ar y topi greu priodwedd newydd. + Math o Ddogfen Feistr + Creu templedi cydweddol + Ychwanegu eicon + + + Trefn + Dyddiad creu + Trefnu wedi'i gwblhau. + Llusgwch yr eitemau gwahanol i fyny neu i lawr isod er mwyn gosod sut dylen nhw gael eu trefnu. Neu cliciwch ar beniadau'r golofnau i drefnu'r holl gasgliad o eitemau + + Nid oes gan y nod hwn nodau plentyn i trefnu + + + Dilysiad + Rhaid i wallau dilysu gael eu trwsio cyn gall yr eitem gael ei achub + Wedi methu + Wedi achub + Diffyg hawliau defnyddiwr, ni ellir cwblhau'r gweithred + Wedi canslo + Gweithred wedi'i ganslo gan ymestyniad 3-ydd parti + Cyhoeddi wedi'i ganslo gan ymestyniad 3-ydd parti + Math o briodwedd yn bodoli eisoes + Math o briodwedd wedi'i greu + Math o ddata: %1%]]> + math o briodwedd wedi'i ddileu + Math o Ddogfen wedi'u achub + Tab wedi'i greu + Tab wedi'i ddileu + Tab gyda id: %0% wedi'i ddileu + Taflen arddull heb ei achub + Taflen arddull wedi'i achub + Taflen arddull wedi'i achub heb unrhyw wallau + Math o ddata wedi'i achub + Eitem geiriadur wedi'i achub + Cyhoeddi wedi methu gan nad yw'r dudalen rhiant wedi'i gyhoeddi + Cynnwys wedi'i gyhoeddi + ac yn weladwy ar y wefan + %0% dogfennau wedi'i gyhoeddi ac yn gweledig ar y wefan + %0% gyhoeddi ac yn gweledig ar y wefan + %0% dogfennau wedi'i gyhoeddi am yr ieithoedd %1% ac yn gweledig ar y wefan + ac yn weladwy ar y wefan tan %0% at %1% + Cynnwys wedi'i achub + Cofiwch gyhoeddi er mwyn i'r newidiadau fod yn weladwy + Mae amserlen ar gyfer cyhoeddi wedi'i diweddaru + %0% wedi arbed + Bydd newidiadau yn cael ei gymerdwyo ar %0% at %1% + Wedi'i anfon am gymeradwyo + Newidiadau wedi'u hanfon am gymeradwyo + %0% newidiadau wedi'u hanfon am gymeradwyo + Cyfrwng wedi'i achub + Grŵp aeloadeth wedi'i achub + Cyfrwng wedi'i achub heb unrhyw wallau + Aelod wedi'i achub + Priodwedd taflen arddull wedi'i achub + Taflen arddull wedi'i achub + Templed wedi'i achub + Gwall yn achub y defnyddiwr (gwiriwch y log) + Defnyddiwr wedi'i achub + math o ddefnyddiwr wedi'i achub + Grŵp defnyddwyr wedi'i achub + Diwylliannau ac enwau gwesteia wedi'i achub + Gwall wrth achub diwylliannau ac enwau gwesteia + Ffeil heb ei achub + Ni ellir achub y ffeil. Gwiriwch hawliau'r ffeil + Ffeil wedi'i achub + Ffeil wedi'i achub heb unrhyw wallau + Iaith wedi'i achub + Math o Gyfrwng wedi'i achub + Math o Aelod wedi'i achub + Grŵp Aelod wedi'i achub + Sgript Python heb ei achub + Ni ellir achub y sgript Python oherwydd gwall + Sgript Python wedi'i achub + Dim gwallau yn y sgript Python + Templed heb ei achub + Sicrhewch nad oes gennych 2 dempled gyda'r un enw arall + Templed wedi'i achub + Templed wedi'i achub heb unrhyw wallau! + XSLT heb ei achub + XSLT yn cynnwys gwall + Ni ellir achub y ffeil XSLT, gwiriwch hawliau ffeil + XSLT wedi'i achub + Dim gwallau yn yr XSLT + Cynnwys wedi'i ddadgyhoeddi + amrywiad cynnwys %0% wedi'i dadgyhoeddi + Roedd yr iaith orfodol '%0%' wedi'i dadgyhoeddi. Mae'r holl ieithoedd ar gyfer yr eitem gynnwys hon bellach wedi'i dadgyhoeddi. + Rhan-wedd wedi'i achub + Rhan-wedd wedi'i achub heb unrhyw wallau! + Rhan-wedd heb ei achub + Bu gwall yn ystod achub y ffeil. + Hawliau wedi'u hachub ar gyfer + Gwedd sgript wedi'i achub + Gwedd sgript wedi'i achub heb unrhyw wallau! + Gwedd sgript heb ei achub + Bu gwall yn ystod achub y ffeil. + Bu gwall yn ystod achub y ffeil. + Wedi dileu %0% o rwpiau defnwyddwr + %0% wedi'i ddileu + %0% o ddefnyddwyr wedi'u galluogi + Bu gwall yn ystod galluogi'r defnyddwyr + Wedi analluogi %0% o ddefnyddwyr + Bu gwall yn ystod analluogi'r defnyddwyr + %0% yn awr wedi galluogi + Bu gwall yn ystod galluogi'r defnyddiwr + %0% yn awr wedi analluogi + Bu gwall yn ystod analluogi'r defnyddiwr + Grwpiau defnyddiwr wedi'u gosod + Wedi dileu %0% o rwpiau defnyddwyr + %0% wedi dileu + Wedi datgloi %0% o ddefnyddwyr + Bu gwall yn ystod datgloi'r defnyddwyr + %0% yn awr wedi datgloi + Bu gwall yn ystod datgloi'r defnyddiwr + Allforwyd yr aelod at ffeil + Bu gwall yn ystod allforio'r aelod + Defnyddiwr %0% wedi'i ddileu + Gawhodd defnyddiwr + Gwahoddiad wedi'i ail-anfon at %0% + Methu cyhoeddi'r ddogfen gan nad yw'r gofynnol '%0%' wedi cael ei gyhoeddi + Methodd dilysiad ar gyfer iaith '%0%' + Mae'r math dogfen wedi ei allforio i ffeil + Digwyddodd gwall wrth allforio'r math dogfen + Ni all y dyddiad rhyddhau fod yn y gorffennol + Ni all drefnu'r ddogfen i'w chyhoeddi gan nad yw'r gofynnol '%0%' wedi cael ei gyhoeddi + Ni all drefnu'r ddogfen i'w chyhoeddi oherwydd mae ganddo'r gofynnol '%0%' ddyddiad cyhoeddi yn hwyrach nag iaith nad yw'n orfodol + Ni all y dyddiad terfyn fod yn y gorffennol + Ni all y dyddiad terfyn fod cyn y dyddiad rhyddhau + + + Yn defnyddio cystrawen CSS e.e: h1, .coch, .glas + Ychwanegu ardull + Golygu ardull + Ardull golygydd testun cyfoethog + Diffiniwch yr arddulliau a ddylai fod ar gael yn y golygydd testun cyfoethog ar gyfer y daflen arddull hon + Golygu taflen arddull + Golygu priodwedd taflen arddull + Enw ar gyfer adnabod y priodwedd arddull yn y golygydd testun gyfoethog + Rhagolwg + Sut fydd y testun yn edrych yn y golygydd testun cyfoethog. + Dewisydd + Yn defnyddio cystrawen CSS e.e: h1, .coch, .glas + Arddulliau + Dyled y CSS ei gymhwyso yn y golygydd testun cyfoethog, e.g. "color:red;" + Côd + Golygydd + + + + Methwyd dileu templed efo'r ID %0% + Golygu templed + Adrannau + Mewnosod ardal cynnwys + Mewnosod dalfan ar gyfer ardal cynnwys + Mewnosod + Dewiswch beth i fewnosod i mewn i'ch templed + Eitem geiriadaur + Mae eitem geiriadur yn ddalfan ar gyfer darn o destun y gall gael ei gyfieithu, sy'n ei wneud yn hawdd i greu dyluniadau ar gyfer gwefannau aml-ieithog. + Macro + + Mae Macro yn gydran ffurfweddol sy'n wych ar gyfer + darnau o'ch dyluniad sy'n cael eu ail-ddefnyddio, ble mae angen y dewis i ddarparu paramedrau, + er enghraifft orielau, ffurflenni a rhestri. + + Gwerth + Yn dangos gwerth maes penodol o'r dudalen bresennol, gyda'r dewisiadau i newid y gwerth neu syrthio'n ôl at werthoedd eraill. + Rhan-wedd + + Mae rhan-wedd yn ffeil templed ar wahân y gall gael ei ddatganu o fewn templed arall, + mae'n wych ar gyfer ail-ddefnyddio côd neu ar gyfer gwahanu templedi cymhleth i mewn i ffeiliau gwahanol. + + Templed Meistr + Dim templed meistr + Dim meistr + + Datganu templed blentyn + + @RenderBody(). + ]]> + + Diffiniwch adran benodol + + @section { ... }. Gall hyn gael ei ddatganu mewn adran + benodol o rhiant y templed yma, wrth ddefnyddio @RenderSection. + ]]> + + Datganu adran benodol + + @RenderSection(name). + mae hyn yn datganu adran o dempled blentyn sydd wedi'i lapio mewn diffiniad berthnasol o @section [name]{ ... }. + ]]> + + Enw Adran + Mae Adran yn ofynnol + @section, fel arall bydd gwall yn cael ei ddangos. + ]]> + Adeiladwr ymholiad + Adeiladu ymholiad + o eitemau wedi dychwelyd, mewn + Copi i'r clipfwrdd + Rydw i eisiau + holl gynnwys + cynnwys o'r fath "%0%" + o + fy wefan + ble + ac + yn + ddim yn + cyn + cyn (gan gynnwys y dyddiad dewiswyd) + ar ôl + ar ôl (gan gynnwys y dyddiad dewiswyd) + yn gyfartal i + ddim yn gyfartal i + yn cynnwys + ddim yn cynnwys + yn fwy na + yn fwy na neu yn gyfartal i + llai na + llai na neu yn gyfartal i + Id + Enw + Dyddiad Creu + Dyddiad Diweddariad Ddiwethaf + trefnu wrth + esgynnol + disgynnol + Templed + + + Golygydd Testun Gyfoethog + Llun + Macro + Mewnosod + Pennawd + Dyfyniad + Dewis math o gynnwys + Dewis cynllun + Ychwanegu rhes + Ychwanegu cynnwys + Gollwng cynnwys + Gosodiadau wedi'u hymgeisio + Nid yw'r cynnwys yma wedi'i ganiatáu yma + Caniateir y cynnwys yma + Cliciwch i fewnblannu + Cliciwch i fewnosod llun + Capsiwn llun... + Ysgrifennwch yma... + Cynlluniau Grid + Cynlluniau yw'r holl ardal weithio gyfan ar gyfer y golygydd grid, fel arfer rydych ddim ond angen un neu ddau gynllun gwahanol + Ychwanegu Cynllun Grid + Golygu Cynllun Grid + Newid y cynllun wrth osod lledau colofnau ac ychwanegu adrannau ychwanegol + Ffurfweddau rhes + Mae rhesi yn gelloedd sydd wedi'u trefnu yn llorweddol + Ychwanegu Ffurfwedd rhes + Golygu Ffurfwedd rhes + Newidiwch y rhes wrth osod lledau colofn ac ychwanegu adrannau ychwanegol + Nid oes ffurfwedd pellach ar gael + Colofnau + Cyfanswm y nifer o golofnau yn y cynllun grid + Gosodiadau + Ffurfweddu pa osodiadau gall olygyddion eu newid + Ardduliau + Ffurfweddu pa arddulliau gall olygyddion eu newid + Bydd gosodiadau dim ond yn newid os mae'r ffurfwedd json yn ddilys + Caniatáu pob golygydd + Caniatáu holl ffurfweddi rhes + Uchafswm o eitemau + Gadewch yn wag neu gosod i 0 ar gyfer diderfyn + Gosod fel diofyn + Dewis ychwanegol + Dewis diofyn + wedi'u hychwanegu + Rhybudd + Rydych chi'n dileu'r ffurfwedd rhes + Bydd dileu enw ffurfwedd rhes yn arwain at golli data ar gyfer unrhyw gynnwys cynfodol sy'n seiliedig ar ffurfwedd hwn. + + + Cyfansoddiadau + Nid ydych wedi ychwanegu unrhyw dabiau + Ychwanegu tab newydd + Ychwanegu tab arall + Grŵp + Nid ydych wedi ychwanegu unrhyw grwpiau + Ychwanegu grŵp + Wedi etifeddu o + Ychwanegu priodwedd + Label gofynnol + Caniatáu gwedd rhestr + Ffurfweddi yr eitem gynnwys i ddangos rhestr trefnadwy a chwiladwy o'i phlant, ni fydd y plant yn cael eu dangos yn y goeden + Templedi Caniateir + Dewiswch pa olygoddion templedi sy'n cael defnyddio cynnwys o'r fath yma + Caniatáu fel gwraidd + Caniatáu golygyddion i greu cynnwys o'r fath yma yng ngwraidd y goeden gynnwys + Iawn - caniatáu cynnwys o'r fath yma yn y gwraidd + Mathau o nod blentyn caniateir + Caniatáu cynnwys o'r mathau benodol i gael eu creu o dan cynnwys o'r fath yma + Dewis nod blentyn + Etifeddu tabiau a phriodweddau o fath o ddogfen sy'n bodoli eisoes. Bydd tabiau newydd yn cael eu ychwanegu at y fath o ddogfen bresennol neu eu cyfuno os mae tab gyda enw yr union yr un fath yn bodoli eisoes. + Mae'r math o gynnwys yma wedi'i ddefnyddio mewn cyfansoddiad, felly ni ellir ei gyfansoddi ei hunan. + Nid oes unrhyw fathau o gynnwys ar gael i'w defnyddio fel cyfansoddiad. + Bydd dileu cyfansoddiad yn dileu'r holl ddata eiddo priodwedd gysylltiedig. Ar ôl i chi arbed y math o ddogfen, bydd ddim ffordd nôl. + Golygyddion ar gael + Ail-ddefnyddio + Gosodiadau golygydd + Ffurfweddau sydd ar gael + Creu ffurfwedd newydd + Ffurfwedd + Iawn, dileu + wedi symud islaw + wedi copïo islaw + Dewiswch y ffolder i symud + Dewiswch y ffolder i gopïo + i yn y strwythyr goeden isod + Holl Fathau o Ddogfennau + Holl Ddogfennau + Holl eitemau gyfrwng + sy'n defnyddio'r fath o ddogfen yma fydd yn cael eu dileu yn barhaol, cadarnhewch os hoffwch ddileu'r rhain hefyd. + sy'n defnyddio'r fath o gyfrwng yma fydd yn cael eu dileu yn barhaol, cadarnhewch os hoffwch ddileu'r rhain hefyd. + sy'n defnyddio'r fath o aelod yma fydd yn cael eu dileu yn barhaol, cadarnhewch os hoffwch ddileu'r rhain hefyd. + a phob dogfen sy'n defnyddio'r fath yma + a phob eitem gyfrwng sy'n defnyddio'r fath yma + a phob aelod sy'n defnyddio'r fath yma + sy'n defnyddio'r golygydd yma fydd yn cael eu diweddaru gyda'r gosodiadau newydd + Aeloed yn gallu golygu + Caniatáu i'r gwerth briodwedd yma gael ei olygu gan yr aelod ar eu tudalen broffil + Yn ddata sensitif + Cuddio'r priodwedd yma o'r golygyddion cynnwys sydd heb hawliau i weld gwybodaeth sensitif + Dangos ar broffil aelod + Caniatáu i'r gwerth briodwedd yma gael ei ddangos ar y dudalen broffil aelod + does dim rhif trefnu gan y tab + Ble mae'r cyfansoddiad yma'n cael ei ddefnyddio? + Mae'r cyfansoddiad yma yn cael ei ddefnyddio'n bresennol yng nghyfansoddiad o'r mathau o gynnwys ganlynol: + Caniatáu amrywiadau + Caniatáu amrywiad yn ôl ddiwylliant + Caniatáu segmentiad + Amrywio gan ddiwylliant + Amrywio gan segmentiad + Caniatáu i olygyddion greu cynnwys o'r math hwn mewn gwahanol ieithoedd + Caniatáu golygyddion i greu cynnwys o ieithoedd gwahanol + Caniatáu golygyddion i greu segmentiadau o'r cynnwys hwn + Caniatáu amrywio yn ôl diwylliant + Caniatáu segmentiad + Math o elfen + Yn fath Elfen + Mae math Elfen i fod i gael ei ddefnyddio er enghraifft mewn Cynnwys Nythu, ac nid yn y goeden + Ni ellir newid math o ddogfen i fath Elfen ar ôl mae'n cael ei defnyddio i greu un neu fwy o eitemau cynnwys. + Nid yw hyn yn berthnasol ar gyfer math Elfen + Rydych wedi gwneud newidiadau i'r eiddo hwn. Ydych chi'n siŵr eich bod chi am eu taflu? + + + Ychwanegu iaith + Iath gorfodol + Rhaid llenwi eiddo ar yr iaith hon cyn y gellir cyhoeddi'r nod. + Iaith diofyn + Gall wefan Umbraco ddim ond cael un iaith ddiofyn. + Gall newid iaith ddiofyn arwain at golli cynnwys diofyn. + Syrthio yn ôl i + Dim iaith cwympo yn ôl + Er mwyn caniatáu i gynnwys amlieithog ddisgyn yn ôl i iaith arall os nad yw'n bresennol yn yr iaith y gofynnwyd amdani, dewiswch hi yma. + Iaith cwympo yn ôl + dim + + + Ychwanegu paramedr + Golygu paramedr + Rhowch enw macro + Paramedrau + Diffiniwch y paramedrau a ddylai fod ar gael wrth ddefnyddio'r macro hwn. + Dewiswch ffeil macro golwg rhannol + + + Adeiladu modelau + gall hyn gymryd amser, peidiwch â phoeni + Modelau wedi'u generadu + Methwyd generadu modelau + Methwyd generadu modelau, gweler yr eithriadau yn y log Umbraco + + + Ychwanegu maes rolio yn ôl + Maes rolio yn ôl + Ychwanegu gwerth diofyn + Gwerth diofyn + Maes rolio yn ôl + Gwerth diofyn + Cyflwr + Amgodiad + Dewis maes + Trawsnewid torriadau llinellau + Iawn, trawsnewid torriadau llinellau + Cyfnewid torriadau llinellau gyda tag html 'br' + Meysydd bersonol + Dyddiad yn unig + Fformat ac amgodiad + Fformatio ar ffurf dyddiad + Fformatio'r gwerth ar ffurf dyddiad, neu dyddiad gyda amser, yn ôl y diwylliant gweithredol + Amgodi HTML + Bydd yn cyfnewid nodau arbennig gyda'u nodau HTML cyfatebol. + Bydd yn cael ei fewnosod ar ôl y gwerth maes + Bydd yn cael ei fewnosod cyn y gwerth maes + Llythrennau bach + Newid allbwn + Dim + Sampl allbwn + Mewnosod ar ôl maes + Mewnosod cyn maes + Ailadroddus + Iawn, gwnewch yn ailadroddus + Gwahanwr + Meysydd Safonol + Llythrennau bras + Amgodi URL + Bydd yn fformatio nodau arbennig o fewn URL + Bydd ddim ond yn cael ei ddefnyddio pan mae'r gwerthoedd maes uchod yn wag + Bydd y maes yma ddim ond yn cael ei ddefnyddio os mae'r maes gynradd yn wag + Dyddiad ac amser + + + Tasgau wedi'u neilltuo i chi + + wedi'u neilltuo i chi. Er mwyn gweld gwedd fanwl gan gynnwys sylwadau, cliciwch ar "Manylion" neu enw'r dudalen. + Gallwch hefyd lawrlwytho'r dudalen ar ffurf XML yn uniongyrchol gan glicio'r ddolen "Lawrlwytho Xml".
+ Er mwyn cau tasg cyfieithu, ewch at y wedd fanylion a cliciwch ar y botwm "Cau". + ]]> +
+ cau tasg + Manylion cyfieithiad + Lawrlwytho pob tasg cyfieithu ar ffurf XML + Lawrlwytho XML + Lawrlwytho XML DTD + Meysydd + Cynnwys is-dudalennau + + + + [%0%] Tasg cyfieithu ar gyfer %1% + Dim defnyddwyr cyfieithu wedi'u darganfod. Creuwch ddefnyddiwr cyfieithu cyn i chi gychwyn anfon cynnwys am gyfieithiadau + Tasgau wedi'u creu gennych chi + + wedi'u creu gennych chi. Er mwyn gweld gwedd fanwl sy'n cynnwys sylwadau, + cliciwch ar "Manylion" neu enw'r dudalen. Gallwch hefyd lawrlwytho'r dudalen ar ffurf XML yn uniongyrchol gan glicio ar y ddolen "Lawrlwytho Xml". + Er mwyn cau tasgau cyfieithu, ewch at y wedd fanylion a cliciwch y botwm "Cau". + ]]> + + Mae'r dudalen '%0%' wedi cael ei anfon am gyfieithiad + Dewiswch yr iaith y dylai'r cynnwys gael ei gyfieithu i + Anfon y dudalen '%0%' am gyfieithiad + Wedi'i neilltuo gan + Tasg wedi'i hagor + Cyfanswm o eiriau + Cyfieithu i + Cyfieithiad wedi'i gwblhau. + Gallwch ragolygu'r tudalennau yr ydych newydd gyfieithu gan glicio isod. Os mae'r dudalen gwreiddiol wedi'i ganfod, byddwch yn cael cymhariaeth o'r 2 dudalen. + Cyfieithiad wedi methu, mae'n bosib fod y ffeil XML wedi llygru + Dewisiadau cyfieithu + Cyfieithydd + Lanlwytho cyfieithiad XML + + + Cynnwys + Templedi Cynnwys + Cyfrwng + Porwr Storfa + Bin Ailgylchu + Pecynnau wedi'u creu + Mathau o Ddata + Geiriadur + Pecynnau wedi'u gosod + Gosod croen + Gosod cit gychwynol + Ieithoedd + Gosod pecyn leol + Macros + Mathau o Gyfrwng + Aelodau + Grwpiau Aelodau + Grwpiau Rolau + Mathau o Aelod + Mathau o Ddogfen + Math o Berthynas + Pecynnydd + Pecynnau + Rhan-weddi + Ffeiliau Rhan-wedd Macro + Ffeiliau Python + Gosod o ystorfa + Gosod Runway + Modylau Runway + Ffeiliau Sgriptio + Sgriptiau + Taflenni arddull + Templedi + Ffeiliau XSLT + Dadansoddeg + Gwyliwr Log + Defnyddwyr + Gosodiadau + Templedi + Trydydd parti + + + Diweddariad newydd yn barod + %0% yn barod, cliciwch yma i lawrlwytho + Dim cysylltiad at y gweinydd + Gwall yn chwilio am ddiweddariad. Ceisiwch wirio'r trywydd stac am fwy o wybodaeth + + + Mynediad + Ar sail y grwpiau aelodaeth ac y nodau cychwyn, mae gan y defnyddiwr hawliau at y nodau ganlynol + Neilltuo hawl + Gweinyddwr + Maes categori + Defnyddiwr wedi'i greu + Newidiwch Eich Cyfrinair + Newidiwch lun + Cyfrinair newydd + ddim wedi cloi allan + Nid yw'r cyfrinair wedi'i newid + Cadarnhau cyfrinair newydd + Gallwch newid eich cyfrinair i gyrchu Swyddfa Gefn Umbracogan lenwi allan y ffurflen isod a chlicio'r botwm 'Newid Cyfrinair' + Sianel Gynnwys + Creu defnyddiwr arall + Creu defnyddwyr newydd i roi hawliau iddynt gyrchu Umbraco. Pan mae defnyddiwr newydd yn cael ei greu, bydd cyfrinair yn cael ei generadu y gallwch chi rannu gyda'r defnyddiwr. + Maes disgrifiad + Analluogi Defnyddiwr + Math o Ddogfen + Golygydd + Maes dyfyniad + Nifer o fethiannau ceisio mewngofnodi + Ewch at broffil defnyddiwr + Ychwanegu grwpiau i neilltuo mynediad a hawliau + Gwahodd defnyddiwr arall + Gwahodd defnyddwyr newydd i roi hawliau iddynt gyrchu Umbraco. Bydd gwahoddiad ebost yn cael ei anfon at y defnyddiwr gyda gwybodaeth ar sut i fewngofnodi i Umbraco. Mae gwahoddiadau yn para am 72 awr. + Iaith + Gosod yr iaith fyddwch chi'n gweld yn y dewislenni a'r deialogau + Dyddiad cloi allan diweddaraf + Mewngofnodi diweddaraf + Cyfrinair wedi'i newid ddiwethaf + Enw defnyddiwr + Nod gychwynol gyfrwng + Cyfyngu'r llyfrgell gyfrwng at nod gychwynol benodol + Nodau gychwynol gyfrwng + Cyfyngu'r llyfrgell gyfrwng at nodau gychwynol benodol + Adrannau + Analluogi Mynediad Umbraco + ddim wedi mewngofnodi eto + Hen gyfrinair + Cyfrinair + Ailosod cyfrinair + Mae eich cyfrinair wedi'i newid! + Cyfrinair wedi'i newid + Cadarnhewch y cyfrinair newydd + Darparwch eich cyfrinair newydd + Ni all eich cyfrinair newydd fod yn wag! + Cyfrinair bresennol + Cyfrinair bresennol annilys + Roedd gwahaniaeth rhwng y cyfrinair newydd ac y cyfrinair i gadarnhau. Ceisiwch eto! + Nid yw'r cyfrinair cadarnhau yn cyfateb â'r cyfrinair newydd! + Cyfnewid hawliau nod blentyn + Rydych ar hyn o bryd yn newid hawliau ar gyfer y tudalennau: + Dewis tudalennau i newid eu hawliau + Dileu llun + Hawliau diofyn + Hawliau gronynnog + Gosod hawliau ar gyfer nodau penodol + Proffil + Chwilio holl blant + Ychwanegu adrannau i roi hawliau i ddefnyddwyr + Dewis grwpiau defnyddwir + Dim nod gychwynol wedi'i ddewis + Dim nodau cychwynol wedi'u dewis + Nod gynnwys gychwynol + Cyfyngu'r goeden gynnwys i nod gychwynol benodol + Nodau cynnwys gychwynol + Cyfyngu'r goeden gynnwys i nodau gychwynol benodol + Defnyddiwr wedi diweddaru ddiwethaf + wedi ei greu + Mae'r defnyddiwr newydd wedi'i greu. Er mwyn mewngofnodi i Umbraco defnyddiwch y cyfrinair isod. + Rheoli defnyddwyr + Enw + Hawliau defnyddiwr + Hawliau grwpiau defnyddiwr + Grŵp defnyddiwr + Grwpiau defnyddiwr + wedi'i wahodd + Mae gwahoddiad wedi cael ei anfon at y defnyddiwr newydd gyda manylion ar sut i fewngofnodi i Umbraco. + Helo a chroeso i Umbraco! Mewn 1 munud yn unig, byddech chi'n barod i fynd, rydym dim ond angen gosod cyfrinair a llun ar gyfer eich avatar. + Croeso i Umbraco! Yn anffodus, mae eich gwahoddiad wedi terfynu. Cysylltwch â'ch gweinyddwr a gofynnwch iddynt ail-anfon. + Lanlwythwch lun i wneud o'n haws i boble eich adnabod chi. + Ysgrifennydd + Cyfieithydd + Newid + Eich proffil + Eich hanes diweddar + Sesiwn yn terfynu mewn + Gwahodd defnyddiwr + Creu defnyddiwr + Anfon gwahoddiad + Yn ôl at ddefnyddwyr + Umbraco: Gwahoddiad + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Helo %0%, +

+

+ Rydych wedi cael eich gwahodd gan %1% i'r Swyddfa Gefn Umbraco. +

+

+ Neges oddi wrth %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Cliciwch y ddolen yma i dderbyn y gwahoddiad + +
+
+

Os na allwch chi glicio ar y ddolen, copiwch a gludwch y URL i mewn i'ch porwr:

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

+
+
+


+
+
+ + ]]> +
+ Gwahoddiad + Yn ail-anfon y gwahoddiad... + Dileu Defnyddiwr + Ydych chi'n sicr eich bod eisiau dileu'r cyfrif defnyddiwr yma? + Pob + Gweithredol + Wedi analluogi + Wedi cloi allan + Wedi gwahodd + Anactif + Enw (A-Y) + Enw (Y-A) + Hynaf + Diweddaraf + Mewngofnodi diweddaraf + No user groups have been added + + + Dilysiad + Dim dilysiad + Dilysu fel cyfeiriad ebost + Dilysu fel rhif + Dilysu fel URL + ...neu darparwch ddilysiad bersonol + Maes yn ofynnol + Darparwch neges gwall dilysiad arferu (opsiynol) + Darparwch fynegiad rheoliadd + Darparwch neges gwall dilysiad arferu (opsiynol) + Mae angen i chi ychwanegu o leiaf + gallwch ddim ond gael + Adio lan i + o eitemau + url(s) + url(s) wedi'i ddewis + o eitemau wedi'u dewis + Dyddiad annilys + Ddim yn rif + Ebost annilys + Ni all y gwerth fod yn null + Ni all y gwerth fod yn gwag + Mae'r gwerth yn annilys, nid yw'n cyfateb i'r patrwm cywir + Dilysiad arferu + %1% mwy.]]> + %1% gormod.]]> + + + + Gwerth wedi'i osod at y gwerth argymhellwyd: '%0%'. + Gwerth wedi'i osod at '%1%' ar gyfer XPath '%2%' yn y ffeil ffurfweddu '%3%'. + Yn disgwyl y gwerth '%1%' ar gyfer '%2%' yn y ffeil ffurfweddu '%3%', ond darganfyddwyd '%0%'. + Darganfyddwyd gwerth annisgwyl '%0%' ar gyfer '%2%' yn y ffeil ffurfweddu '%3%'. + + Gwallau bersonol wedi gosod at '%0%'. + Gwallau bersonol wedi gosod at '%0%' yn bresennol. Argymhellwyd i osod hyn i '%1%' cyn mynd yn fyw. + Gwallau bersonol wedi gosod at '%0%' yn llwyddiannus. + + Gwallau Macro wedi gosod at '%0%'. + Gwallau Macro wedi gosod at '%0%' a fydd yn atal rhai neu holl dudalennau yn eich safle rhag llwytho'n gyfan gwbl os oes unrhyw wallau o fewn macros. Bydd cywiro hyn yn gosod y gwerth at '%1%'. + Gwallau Macro wedi gosod at '%0%' yn llwyddiannus. + + Ceisio sgipio Gwallau IIS Bersonol wedi'i osod at '%0%' ac rydych yn defnyddio fersiwn IIS '%1%'. + Ceisio sgipio Gwallau IIS Bersonol wedi'i osod at '%0%'. Argymhellwyd gosod hyn at '%1%' ar gyfer eich fersiwn IIS (%2%). + Ceisio sgipio Gwallau IIS Bersonol wedi'i osod at '%0%' yn llwyddiannus. + + Ffeil ddim yn bodoli: '%0%'. + '%0%' yn y ffeil ffurfweddu '%1%'.]]> + Bu gwall, gwiriwch y log ar gyfer y gwall cyflawn: %0%. + Aelodau - Cyfanswm XML: %0%, Cyfanswm: %1%, Cyfanswm annilys: %2% + Cyfrwng - Cyfanswm XML: %0%, Cyfanswm: %1%, Cyfanswm annilys: %2% + Cynnwys - Cyfanswm XML: %0%, Cyfanswm wedi cyhoeddi: %1%, Cyfanswm annilys: %2% + Cronfa ddata - Mae'r sgema gronfa ddata yn gywir ar gyfer y fersiwn yma o Umbraco + %0% o broblemau wedi'u canfod gyda'ch sgema gronfa ddata (Gwiriwch y log am fanylion) + Darganfyddwyd gwallau wrth ddilysu'r sgema gronfa ddata yn erbyn y fersiwn bresennol o Umbraco. + Mae tystysgrif eich gwefan yn ddilys. + Gwall dilysu tystysgrif: '%0%' + Mae tystysgrif SSL eich gwefan wedi terfynu. + Mae tystysgrif SSL eich gwefan am derfynu mewn %0% diwrnod. + Gwall yn pingio'r URL %0% - '%1%' + Rydych yn bresennol %0% yn gweld y wefan yn defnyddio'r cynllun HTTPS. + Mae'r appSetting 'umbracoUseSSL' wedi'i osod at 'false' yn eich ffeil web.config. Unwaith rydych yn ymweld â'r safle gan ddefnyddio'r cynllun HTTPS, dylai hynny gael ei osod i 'true'. + Mae'r appSetting 'umbracoUseSSL' wedi'i osod at '%0%' yn eich ffeil web.config, mae eich cwcis %1% marcio yn ddiogel. + Ni ellir diweddaru'r gosodiad 'umbracoUseSSL' yn eich ffeil web.config. Gwall: %0% + + Galluogi HTTPS + Yn gosod umbracoSSL i true yn yr appSettings yn y ffeil web.config. + Mae'r appSetting 'umbracoUseSSL' yn awr wedi'i osod at 'true' yn eich ffeil web.config, bydd eich cwcis wedi eu marcio yn ddiogel. + Trwsio + Ni ellir trwsio gwiriad gyda math chymhariaeth gwerth o 'ShouldNotEqual'. + Ni ellir trwsio gwiriad gyda math chymhariaeth gwerth o 'ShouldEqual' gyda gwerth a ddarparwyd. + Gwerth i drwrsio gwiriad heb ei ddarparu. + Modd casgliad dadfygio wedi'i analluogi. + Modd casgliad dadfygio wedi'i alluogi. Argymhellwyd analluogi'r gosodiad yma cyn mynd yn fyw. + Modd casgliad dadfygio wedi'i analluogi yn llwyddiannus. + Modd olrhain wedi'i analluogi. + Modd olrhain wedi'i alluogi. Argymhellwyd analluogi'r gosodiad yma cyn mynd yn fyw. + Modd olrhain wedi'i analluogi yn llwyddiannus. + Mae gan pob ffolder yr hawliau cywir wedi'u gosod. + + %0%.]]> + %0%. Os nad ydyn nhw'n cael eu ysgrifennu atynt, does dim angen unrhyw weithred.]]> + Mae gan pob ffeil yr hawliau cywir wedi'u gosod. + + %0%.]]> + %0%. Os nad ydyn nhw'n cael eu ysgrifennu atynt, does dim angen unrhyw weithred.]]> + X-Frame-Options sy'n cael ei ddefnyddio i reoli os mae safle'n gallu cael ei osod o fewn IFRAME gan safle arall wedi'i ganfod.]]> + X-Frame-Options sy'n cael ei ddefnyddio i reoli os mae safle'n gallu cael ei osod o fewn IFRAME gan safle arall wedi'i ganfod.]]> + Gosod Peniad o fewn Ffurfwedd + Ychwanegu gwerth at yr adran httpProtocol/customHeaders o'r ffeil web.config er mwyn atal y safle rhag cael ei ddangos o fewn IFRAME gan safleoedd eraill. + Gosodiad ar gyfer creu peniad sy'n atal y wefan rhag cael ei ddangos o fewn IFRAME ar safle arall wedi'i ychwanegu at eich ffeil web.config. + Ni ellir diweddaru'r ffeil web.config. Gwall: %0% + X-Content-Type-Options sy'n cael ei ddefnyddio i amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ganfod.]]> + X-Content-Type-Options sy'n cael ei ddefnyddio i amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ganfod.]]> + Ychwanegu gwerth at yr adran httpProtocol/customHeaders o'r ffeil web.config er mwyn amddiffyn yn erbyn gwendidau sniffio MIME. + Gosodiad ar gyfer creu peniad sy'n amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ychwanegu at eich ffeil web.config. + Strict-Transport-Security, hefyd wedi'i adnabod fel HSTS-header, wedi'i ganfod.]]> + Strict-Transport-Security wedi'i ganfod.]]> + Ychwanegu'r peniad 'Strict-Transport-Security' gyda'r gwerth 'max-age=10886400; preload' i'r adran httpProtocol/customHeaders o'r ffeil web.config. Defnyddiwch y trwsiad hyn dim ond os bydd gennych chi eich parthau yn rhedeg gyda https am yr 18 wythnos nesaf (o leiaf). + Mae'r peniad HSTS wedi'i ychwanegu at y ffeil web.config. + X-XSS-Protection wedi'i ganfod.]]> + X-XSS-Protection wedi'i ganfod.]]> + Ychwanegu'r peniad 'X-XSS-Protection' gyda'r gwerth '1; mode=block' at yr adran httpProtocol/customHeaders yn y ffeil web.config. + Mae'r peniad X-XSS-Protection wedi'i ychwanegu at y ffeil web.config. + + %0%.]]> + Dim peniadau sy'n datgelu gwynodaeth am dechnoleg eich gwefan wedi'u canfod. + Ni ellir darganfod system.net/mailsettings yn y ffeil Web.config. + Yn yr adran system.net/mailsettings o'r ffeil Web.config, nid yw'r "host" wedi ffurfweddu. + Gosodiadau SMTP wedi ffurfweddu'n gywir ac mae'r gwasanaeth yn gweithio fel y disgwylir. + Ni ellir cysylltu â gweinydd SMTP sydd wedi ffurfweddu gyda "host" '%0%' a phorth '%1%'. Gwiriwch fod y gosodiadau SMTP yn y ffeil Web.config, system.net/mailsettings yn gywir. + %0%.]]> + %0%.]]> +

Canlyniadau'r gwiriad Statws Iechyd Umbraco ar amserlen rhedwyd ar %0% am %1% fel y ganlyn:

%2%]]>
+ Statws Iechyd Umbraco: %0% + Gwiriwch Pob Grŵp + Gwiriwch y grŵp + + Mae'r gwiriwr iechyd yn gwerthuso gwahanol rannau o'ch gwefan ar gyfer gosodiadau arfer gorau, cyfluniad, problemau posibl, ac ati. Gallwch chi drwsio problemau yn hawdd trwy wasgu botwm. + Gallwch chi ychwanegu eich gwiriadau iechyd eich hun, edrych ar y ddogfennaeth i gael mwy o wybodaeth am wiriadau iechyd arferu.

+ ]]> +
+ Eich wefan gallu defnyddio y protocol gwarchodaeth TLS 1.2 wrth wneud cysylltiadau allanol i endpoints HTTPS + Nid yw'ch gwefan wedi'i ffurfweddu i ganiatáu protocol diogelwch TLS 1.2 wrth wneud cysylltiadau allanol: efallai na fydd modd cyrchu rhai endpoints HTTPS gan ddefnyddio protocol llai diogel. + + + Analluogi olinydd URL + Galluogi olinydd URL + Diwylliant + URL gwreiddiol + Ailgyfeirwyd I + Gweinyddu Ailgyfeirio URLs + Mae'r URLs ganlynol yn ailgyfeirio at yr eitem gynnwys yma: + Dim ailgyfeiriadau wedi'u gwneud + Pan mae tudalen wedi'i gyhoeddi yn cael ei ailenwi neu symud bydd ailgyfeiriad yn cael ei greu yn awtomatig at y dudalen newydd. + Dileu + Ydych chi'n sicr eich bod eisiau dileu'r ailgyfeiriad o '%0%' at '%1%'? + URL ailgyfeirio wedi'i ddileu. + Gwall yn dileu'r URL. + Bydd hyn yn dileu'r ailgyfeiriad + Ydych chi'n sicr eich bod eisiau analluogi'r olinydd URL? + Mae'r olinydd URL wedi cael ei analluogi. + Gwall yn ystod analluogi'r olinydd URL, gall fwy o wybodaeth gael ei ddarganfod yn eich ffeil log. + Mae'r olinydd URL wedi cael ei alluogi. + Gwall yn ystod galluogi'r olinydd URL, gall fwy o wybodaeth gael ei ddarganfod yn eich ffeil log. + + + Dim eitemau Geiriadur i ddewis ohonynt + + + o nodau ar ôl + %1% gormod.]]> + + + Wedi chwalu cynnwys gyda Id: {0} yn berthnasol i gynnwys rhiant gwreiddiol gyda Id: {1} + Wedi chwalu cyfrwng gyda Id: {0} yn berthnasol i gyfrwng rhiant gwreiddiol gyda Id: {1} + Ni ellir adfer yr eitem yma yn awtomatig + Nid oes unrhyw leoliad lle gellir adfer yr eitem hon yn awtomatig. Gallwch chi symud yr eitem â llaw gan ddefnyddio'r goeden isod. + oedd adferwyd o dan + Does dim perthynas 'adfer' ar gael ar gyfer y nod yma. Defnyddiwch y ddewislen Symud i'w symud â llaw. + Mae'r eitem yr ydych eisiau adfer yr item oddi tan ('%0%') yn y bin ailgylchu. Defnyddiwch y ddewislen Symud i'w symud â llaw. + + + Cyfeiriad + Rhiant i plentyn + Deugyfeiriadol + Rhiant + Plentyn + Cyfrif + Cysylltiadau + Creu + Sylw + Enw + Dim cysylltiadau ar gyfer y math hwn o berthynas. + Math o Berthynas + Cysylltiadau + + + Dechrau Arni + Rheolaeth Ailgyfeirio URL + Cynnwys + Croeso + Rheolaeth Examine + Statws Cyhoeddedig + Adeiladwr Modelau + Gwiriad Iechyd + Proffilio + Dechrau Arni + Gosod Ffurflenni Umbraco + + + Mynd yn ôl + Cynllun gweithredol: + Neidio i + grŵp + pasio + rhybudd + methu + awgrym + Gwiriad wedi'i basio + Gwiriad wedi'i methu + Agor chwiliad swyddfa gefn + Agor/Cau cymorth swyddfa gefn + Agor/Cau eich opsiynau proffil + Sefydli Diwylliannau ac Enwau Gwesteia am %0% + Creu nod newydd o dan %0% + Sefydli Mynediad Cyhoeddus ar %0% + Sefydli Caniataid ar %0% + Newid y trefniad am %0% + Creu templed cynnwys yn seiliedig ar %0% + Agor dewislen cyd-destun ar gyfer + Iaith gyfredol + Newid iaith i + Creu ffolder newydd + Golwg Rhannol + Macro Golwg Rhannol + Aelod + Math o ddata + Chwilio'r dangosfwrdd ailgyfeirio + Chwilio'r adran grŵp defnyddwyr + Chwilio'r adran defnyddwyr + Creu eitem + Creu + Golygu + Enw + Ychwanegu rhes newydd + Gweld mwy o opsiynau + Wedi cyfieithu + Cyfieithiad ar goll + Eitemau geiriadur + + + Cyfeiriadau + This Data Type has no references. Nid oes gan y Math o Ddata hwn unrhyw gyferiadau. + Defnyddir mewn Mathau o Ddogfennau + Ddim cyfeiriadau i Fathau o Ddogfennau. + Defnyddir mewm Mathau o Gyfrwng + Ddim cyfeiriadau i Fathau o Gyfrwng. + Defnyddir mewn Mathau o Aelod + Ddim cyfeiriadau i Fathau o Aelod. + Defnyddir gan + A ddefnyddir yn Ddogfennau + A ddefnyddir yn Aelodau + A ddefnyddir yn Cyfryngau + + + Dileu Chwiliad Cadwedig + Lefelau Log + Dewiswch y cyfan + Dad-ddewiswch bawb + Chwiliadau Cadwedig + Arbed Chwiliad + Rhoi enw cyfeillgar am eich ymholiad chwilio + Hidlo Chwiliad + Cyfanswm o Eitemau + Stamp Amser + Lefel + Peiriant + Neges + Eithriad + Priodweddau + Chwilio efo Google + Chwiliwch y neges hon efo Google + Chwilio efo Bing + Chwiliwch y neges hon efo Bing + Chwilio Our Umbraco + Chwiliwch y neges hon arno Our Umbraco fforymau a dogfennau + Chwilio Our Umbraco efo Google + Chwilio Our Umbraco fforymau efo Google + Chwilio'r cod gwreiddiol Umbraco + Chwilio tu fewn y cod gwreiddiol Umbraco ar Github + Chwilio Problemau Umbraco + Chwilio Problemau Umbraco ar Github + Dileu chwiliad hon + Darganfod logiau efo ID y Cais + Darganfod logiau efo Namespace + Darganfod logiau efo Enw Peiriant + Agor + + + Copi %0% + %0% o %1% + Dileu pob eitem + Clirio y clipfwrdd + + + Agor Gweithredoedd Priodweddau + Cau Gweithredoedd Priodweddau + + + Aros + Adnewyddu statws + Cuddstôr Cof + + + + Ail-lwytho + Cuddstôr Cronfa Ddata + + Gall ailadeiladu fod yn ddrud. + Defnyddio fo pan mae ail-lwytho ddim yn ddigon, a ti'n feddwl mai'r stôr cronfa ddata heb gael ei + chynhyrchu'n iawn—a fyddai'n arwydd o broblem gritigol efo Umbraco. + ]]> + + Ailadeiladu + Mewnol + + nad oes angeni chi ei defnyddio. + ]]> + + Casglu + Statws Cuddstôr Cyhoeddedig + Cuddstorau + + + Proffilio perfformiad + + + Mae Umbraco yn rhedeg mewn modd dadfygio. Mae hyn yn golygu y gallwch chi ddefnyddio'r proffiliwr perfformiad adeiledig i asesu'r perfformiad wrth rendro tudalennau. +

+

+ OS ti eisiau actifadu'r proffiliwr am rendro tudalen penodol, bydd angen ychwanegu umbDebug=true i'r ymholiad wrth geisio am y tudalen +

+

+ Os ydych chi am i'r proffiliwr gael ei actifadu yn ddiofyn am bob rendrad tudalen, gallwch chi ddefnyddio'r togl isod. + Bydd e'n gosod cwci yn eich porwr, sydd wedyn yn actifadu'r proffiliwr yn awtomatig. + Mewn geiriau eraill, bydd y proffiliwr dim ond yn actif yn ddiofyn yn eich porwr chi - nid porwr pawb eraill. +

+ ]]> +
+ Actifadu y proffiliwr yn ddiofyn + Nodyn atgoffa cyfeillgar + + + Ni ddylech chi fyth adael i safle cynhyrchu redeg yn y modd dadfygio. Mae'r modd dadfygio yn gallu cael ei diffodd trwy ychwanegu'r gosodiad debug="false" ar yr elfen <grynhoi /> yn web.config. +

+ ]]> +
+ + + Mae Umbraco ddim yn rhedeg mewn modd dadfygio ar hyn o bryd, felly nid allwch chi ddefnyddio'r proffiliwer adeiledig. Dyma sut y dylai fod ar gyfer safle cynhyrchu. +

+

+ Mae'r modd dadfygio yn gallu cael ei throi arno gan ychwanegu'r gosodiad debug="true" ar yr elfen <grynhoi /> yn web.config. +

+ ]]> +
+ + + Oriau o fideos hyfforddiant Umbraco ddim ond un clic i fwrdd + + Eisiau meistroli Umbraco? Treuliwch gwpl o funudau yn dysgu rhai o'r arferion gorau gan wylio un o'r fideos hyn am sut i ddefnyddio Umbraco. Ac ymweld â umbraco.tv am fwy o fideos am Umbraco

+ ]]> +
+ I roi cychwyn i chi + + + Dechrau yma + Mae'r adran hon yn cynnwys y blociau adeiladu am eich safle Umbraco. Dilyn y dolenni isod i ddarganfod fwy am weithio gyda'r eitemau yn yr adran Gosodiadau + Ddarganfod fwy + + fewn yr adran Dogfennaeth o Our Umbraco + ]]> + + + Fforwm Cymunedol + ]]> + + + fideos tiwtorial (mae rhai am ddim, ond bydd angen tanysgrifiad am rhai eraill) + ]]> + + + hoffer hybu cynhyrchiant a chefnogaeth fasnachol + ]]> + + + hyfforddi ac ardystio + ]]> + + + + Croeso i'r SRC cyfeillgar + Diolch am ddewis Umbraco - rydyn ni'n credu y gallai hyn fod dechreuad i rywbeth prydferth. Er y gallai deilo'n llethol ar y dechrau, rydym wedi gwneud llawer i wneud y gromlin ddysgu mor llyfn a chyflym a phosib. + + + Ffurflenni Umbraco + Creu ffurflenni gan ddefnyddio rhyngwyneb llusgo a gollwng sythweledol. O ffurflenni cyswllt syml sy'n anfon e-byst, i holiaduron mwy datblygedig sy'n integreiddio efo systemau CRM. Bydd eich cleientiaid wrth ei modd! + + + Creu bloc newydd + Atodwch adran gosodiadau + Dewis golygfa + Dewis taflen arddull + Dewis delwedd bawd + Creu newydd + Taflen arddull arferu + Ychwanegu taflen arddull + Ymddangosiad y golygydd + Modelau data + Ymddangosiad y catalog + Lliw cefndir + Lliw eicon + Model Cynnwys + Label + Golygfa arferu + Ddangos disgrifiad golygfa arferu + Trosysgrifo sut mae'r bloc hwn yn ymddangos yn yr UI y swyddfa gefn. Dewis ffeil .html sy'n cynnwys eich cyflwyniad. + Model gosodiadau + Maint y golygydd troshaen + Ychwanegu golygfa arferu + Ychwanegu gosodiadau + Trosysgrifo templed label + %0%.]]> + %0%.]]> + Bydd cynnwys y bloc hwn yn dal i fod yn bresennol, ni fydd golygu'r cynnwys hwn ar gael mwyach a bydd yn cael ei ddangos fel cynnwys heb gefnogaeth. + + Delwedd bawd + Ychwanegu delwedd bawd + Creu gwag + Clipfwrdd + Gosodiadau + Datblygedig + Gorfodi cuddio'r golygydd cynnwys + Rydych chi wedi gwneud newidiadau i'r cynnwys hwn. Wyt ti'n siŵr eich bod chi am eu taflu ei fwrdd? + Gwaredu cread? + + Priodwedd '%0%' yn defnyddio'r golygydd '%1%' sydd ddim yn cael ei gefnogi mewn blociau. + + + Beth yw Templedi Gynnwys + Mae Templedi Gynnwys yn gynnwys cyn-diffiniedig sydd yn gallu cael ei ddewis wrth greu nod cynnwys newydd. + Sut ydw i'n creu Templed Gynnwys? + + Mae yna ddwy ffordd i greu Templed Gynnwys:

+
    +
  • Gliciwch-de ar nod cynnwys a dewis "Creu Templed Gynnwys" i greu Templed Gynnwys newydd.
  • +
  • Gliciwch-de ar y goeden Templedi Gynnwys yn yr adran Gosodiadau a dewis y Math of Dogfen ti eisiau creu Templed Gynnwys am.
  • +
+

Unwaith y rhoddir enw, gall golygyddion ddechrau defnyddio'r Templed Gynnwys fel sylfaen am ei thudalen newydd.

+ ]]> +
+ Sut ydw i'n rheoli Templedi Gynnwys + Gallwch chi olygu a dileu Templedi Gynnwys o'r goeden "Templedi Gynnwys" yn yr adran Gosodiadau. Ehangwch y Math o Ddogfen mae'r Templed Gynnwys yn seiliedig arno a chlicio fo i'w golygu neu ddileu. + +
diff --git a/TestSite/Umbraco/Config/Lang/da.xml b/TestSite/Umbraco/Config/Lang/da.xml index 158b333..d08f0a9 100644 --- a/TestSite/Umbraco/Config/Lang/da.xml +++ b/TestSite/Umbraco/Config/Lang/da.xml @@ -8,7 +8,8 @@ Tilføj domæne Revisionsspor Gennemse elementer - Skift dokumenttype + Skift Dokument Type + Skift Input Type Kopier Opret Eksportér @@ -16,6 +17,7 @@ Opret gruppe Slet Deaktivér + Edit settings Tøm papirkurv Aktivér Eksportér dokumenttype @@ -29,6 +31,7 @@ Udgiv Afpublicér Genindlæs elementer + Fjern Genudgiv hele sitet Omdøb Gendan @@ -53,6 +56,7 @@ Sæt rettigheder Lås op Opret indholdsskabelon + Gensend Invitation Standard værdi @@ -139,23 +143,29 @@ Gem og send til udgivelse Gem listevisning Planlæg - Forhåndsvisning + Se side + Forhåndsvisning Forhåndsvisning er deaktiveret fordi der ikke er nogen skabelon tildelt Vælg formattering Vis koder Indsæt tabel + Generer modeller og luk Gem og generer modeller Fortryd Genskab + Rul tilbage Slet tag Fortryd Bekræft - Flere publiseringsmuligheder + Flere publiseringsmuligheder + Indsæt + Indsæt og luk For Brugeren har slettet indholdet Brugeren har afpubliceret indholdet + Brugeren har afpubliceret indholdet for sprogene: %0% Brugeren har gemt og udgivet indholdet Brugeren har gemt og udgivet indholdet for sprogene: %0% Brugeren har gemt indholdet @@ -164,8 +174,10 @@ Brugeren har kopieret indholdet Brugeren har tilbagerullet indholdet til en tidligere tilstand Brugeren har sendt indholdet til udgivelse + Brugeren har sendt indholdet til udgivelse for sprogene: %0% Brugeren har sendt indholdet til oversættelse Brugeren har sorteret de underliggende sider + %0% Kopieret Udgivet Udgivet @@ -174,32 +186,15 @@ Gemt Slettet Afpubliceret + Afpubliceret Indhold tilbagerullet Sendt til udgivelse + Sendt til udgivelse Sendt til oversættelse Sorteret + Brugerdefineret Historik (alle sprog) - - For at skifte det valgte indholds dokumenttype, skal du først vælge en ny dokumenttype, som er gyldig på denne placering. - Kontroller derefter, at alle egenskaber bliver overført rigtigt til den nye dokumenttype, og klik derefter på Gem. - Indholdet er blevet genudgivet. - Nuværende egenskab - Nuværende type - Du kan ikke skifte dokumenttype, da der ikke er andre gyldige dokumenttyper på denne placering. - Dokumenttype skiftet - Overfør egenskaber - Overfør til egenskab - Ny skabelon - Ny type - ingen - Indhold - Vælg ny dokumenttype - Dokumenttypen på detvalgte indhold blev skiftet til [new type], og følgende egenskaber blev overført: - til - Overførsel af egenskaber kunne ikke fuldføres, da en eller flere egenskaber er indstillet til at blive overført mere end én gang. - Kun andre dokumenttyper, der er gyldige på denne placering, vises. - Oprettelse af mappen under parent med ID %0% fejlede Oprettelse af mappen under parent med navnet %0% fejlede @@ -237,16 +232,17 @@ Ingen dato valgt Sidetitel Dette medie har ikke noget link + Intet indhold kan tilføjes for dette element Egenskaber Dette dokument er udgivet, men ikke synligt da den overliggende side '%0%' ikke er udgivet! Dette sprog er udgivet, men ikke synligt, da den overliggende side '%0%' ikke er udgivet! Ups: dette dokument er udgivet, men er ikke i cachen (intern fejl) - Kunne ikke hente url'en - Dette dokument er udgivet, men dets url ville kollidere med indholdet %0% + Kunne ikke hente URL'en + Dette dokument er udgivet, men dets URL ville kollidere med indholdet %0% Dette dokument er udgivet, men dets URL kan ikke dirigeres Udgiv Udgivet - Udgivet (Afventende ændringer) + Udgivet (Ventede ændringer) Udgivelsesstatus Udgiv med undersider for at udgive %0% og alle sider under og dermed gøre deres indhold offentligt tilgængelige.]]> Udgiv med undersider for at udgive de valgte sprog og de samme sprog for sider under og dermed gøre deres indhold offentligt tilgængelige.]]> @@ -259,29 +255,37 @@ Statistik Titel (valgfri) Alternativ tekst (valgfri) + Overskrift (valgfri) Type + Hvilke varianter vil du udgive? + Vælg hvilke varianter, der skal gemmes. Afpublicér Afpubliceret Ikke oprettet Sidst redigeret Tidspunkt for seneste redigering Fjern fil + Klik her for at fjerne billedet fra medie filen + Klik her for at fjerne filen fra medie filen Link til dokument Medlem af grupper(ne) Ikke medlem af grupper(ne) Undersider Åben i vindue Dette oversætter til den følgende tid på serveren: - Hvad betyder det?]]> + Hvad betyder det?]]> Er du sikker på, at du vil slette dette element? Er du sikker på, at du vil slette alle elementer? Egenskaben %0% anvender editoren %1% som ikke er understøttet af Nested Content. Der er ikke konfigureret nogen indholdstyper for denne egenskab. + Tilføj element type + Vælg element type + Vælg gruppen, hvis værdier skal vises. Hvis dette er efterladt blankt vil den første gruppe på element typen bruges. %0% fra %1% Tilføj en ny tekstboks Fjern denne tekstboks Indholdsrod - Medtag udkast: udgiv også ikke-publicerede sider. + Inkluder ikke-udgivet indhold. Denne værdi er skjult.Hvis du har brug for adgang til at se denne værdi, bedes du kontakte din web-administrator. Denne værdi er skjult. Hvilke sprog vil du gerne udgive? Alle sprog med indhold gemmes! @@ -292,15 +296,25 @@ Hvilke sprog vil du gerne planlægge? Vælg sproget du vil afpublicere. Afpublicering af et obligatorisk sprog vil afpublicere alle sprog. Udgivne sprog + Nulstil fokuspunkt Ikke-udgivne sprog Uændrede sprog Disse sprog er ikke blevet oprettet + Alle nye varianter vil blive gemt. + Hvilke varianter skal udgives? + Vælg, hvilke varianter skal gemmes. + Vælg varianter som skal sendes til gennemgang. + Sæt udgivnings tidspunkt... + Vælg varianterne som skal afpubliceres. Afpublicering af et krævet sprog vil afpublicere alle varianter. + De følgende varianter er krævet for at en udgivelse kan finde sted: + Vi er ikke klar til at udgive Klar til at udgive? Klar til at gemme? Send til godkendelse Vælg dato og klokkeslæt for at udgive og/eller afpublicere indholdet. Opret ny Indsæt fra udklipsmappen + Dette element er i papirkurven Opret en ny indholdsskabelon fra '%0%' @@ -319,16 +333,30 @@ Maks filstørrelse er Medie rod Flytning af mediet fejlede + Overordnet og destinations mappe kan ikke være den samme Kopiering af mediet fejlede Oprettelse af mappen under parent med id %0% fejlede Omdøbning af mappen med id %0% fejlede - Træk dine filer ind i dropzonen for, at uploade dem til mediebiblioteketet. + Træk dine filer ind i dropzonen for, at uploade dem til mediebiblioteket. + Upload er ikke tiladt på denne lokation Opret et nyt medlem Alle medlemmer Medlemgrupper har ingen yderligere egenskaber til redigering. + + Kopiering af indholdstypen fejlede + Flytning af indholdstypen fejlede + + + Kopiering af medietypen fejlede + Flytning af medietypen fejlede + Auto vælg + + + Kopiering af medlemstypen fejlede + Hvor ønsker du at oprette den nye %0% Opret under @@ -340,10 +368,21 @@ Den valgte side i træet tillader ikke at sider oprettes under den. Rediger tilladelser for denne dokumenttype. Opret en ny dokumenttype + Dokumenttyper inde i Indstillinger sektionen, ved at ændre Tillad på rodniveau indestillingen under Permissions.]]> "media typer".]]> Det valgte medie i træet tillader ikke at medier oprettes under det. Rediger tilladelser for denne medietype. Dokumenttype uden skabelon + Dokumenttype med skabelon + Definerer en indholdsside, der kan oprettes af redaktørerne i indholdstræet, og som er kan tilgås direkte på en URL. + Dokumenttype + Definerer en indholdskomponent, der kan oprettes af redaktørerne i indholdstræet og benyttes i sammenhæng med andet indhold, men som ikke kan tilgås direkte på en URL. + Element-type + Definerer skabelonen for et sæt at egenskaber, der kan anvendes som skema i avancerede felter som f.eks. 'Block List' eller 'Nested Content'. + Komposition + Definerer et sæt genbrugbare egenskaber, der kan inkluderes i definitionen af andre dokumenttyper - f.eks. et sæt 'Almindelige side-data'. + Mappe + Benyttes til at organisere dokumenttyper, element-typer og kompositioner i dokumenttype-træet. Ny mappe Ny datatype Ny JavaScript-fil @@ -406,6 +445,9 @@ Luk denne dialog Er du sikker på at du vil slette Er du sikker på du vil deaktivere + Er du sikker på at du vil fjerne + %0%]]> + %0%]]> Er du sikker på at du vil forlade Umbraco? Er du sikker? Klip @@ -420,11 +462,13 @@ Indsæt makro Indsæt tabel Dette vil slette sproget + Ændring af kulturen for et sprog kan forsage en krævende opration og vil resultere i indholds cache og indeksering vil blive genlavet Sidst redigeret Link Internt link: Ved lokalt link, indsæt da en "#" foran linket Åben i nyt vindue? + Makroindstillinger Denne makro har ingen egenskaber du kan redigere Indsæt tekst Rediger rettigheder for @@ -434,8 +478,8 @@ Elementerne i papirkurven slettes. Luk venligst ikke dette vindue mens sletningen foregår Papirkurven er nu tom Når elementer slettes fra papirkurven, slettes de for altid - regexlib.com's webservice oplever i øjeblikket problemer, vi ikke har kontrol over. Beklager ulejligheden. ]]> - Søg efter et regulært udtryk for at tilføje validering til et formularfelt. Eksempel: 'e-mail', 'postnr.', 'url' + regexlib.com's webservice oplever i øjeblikket problemer, vi ikke har kontrol over. Beklager ulejligheden. ]]> + Søg efter et regulært udtryk for at tilføje validering til et formularfelt. Eksempel: 'e-mail', 'postnr.', 'URL' Fjern makro Obligatorisk Sitet er genindekseret @@ -467,6 +511,7 @@ Vælg medlemstype Vælg node Vælg sektioner + Vælg bruger Vælg brugere Ingen ikoner blev fundet Der er ingen parametre for denne makro @@ -479,8 +524,12 @@ Fjern link fra dit konto Vælg editor + Vælg konfiguration Vælg snippet Dette vil slette noden og alle dets sprog. Hvis du kun vil slette et sprog, så afpublicér det i stedet. + %0%]]> + %0% fra gruppen]]> + Ja, fjern Der er ingen ordbogselementer. @@ -520,7 +569,7 @@ felter Indexet skal bygges igen, for at kunne læses Processen tager længere tid end forventet. Kontrollér Umbraco loggen for at se om der er sket fejl under operationen - Dette index kan ikke genbygess for det ikke har nogen + Dette index kan ikke genbygges for det ikke har nogen IIndexPopulator @@ -542,6 +591,9 @@ #value eller ?key=value Indtast alias... Genererer alias... + Opret element + Rediger + Navn Opret brugerdefineret listevisning @@ -606,6 +658,7 @@ Denne egenskab er ugyldig + Valgmuligheder Om Handling Muligheder @@ -623,6 +676,7 @@ Ryd Luk Luk vindue + Luk vindue Kommentar Bekræft Proportioner @@ -631,6 +685,7 @@ Fortsæt Kopiér Opret + Beskær sektion Database Dato Standard @@ -640,6 +695,7 @@ Design Ordbog Dimensioner + Kassér Ned Hent Rediger @@ -662,6 +718,7 @@ Id Importer Inkludér undermapper i søgning + Søg kun i denne mappe Info Indre margen Indsæt @@ -731,7 +788,7 @@ Opdatér Opdatér Upload - Url + URL Bruger Brugernavn Værdi @@ -754,6 +811,8 @@ Andet Artikler Videoer + installere + Avatar til Blå @@ -771,15 +830,16 @@ Vis genveje Brug listevisning Tillad på rodniveau - Comment/Uncomment lines - Remove line - Copy Lines Up - Copy Lines Down - Move Lines Up - Move Lines Down + Kommentér/Udkommentér linjer + Slet linje + Kopiér linjer op + Kopiér linjer ned + Flyt linjer op + Flyt linjer ned Generelt Editor Skift tillad sprogvarianter + Skift tillad segmentering Baggrundsfarve @@ -801,7 +861,7 @@ ]]> Næste for at fortsætte.]]> Databasen er ikke fundet. Kontrollér venligst at informationen i database forbindelsesstrengen i "web.config" filen er korrekt.

-

For at fortsætte bedes du venligst rette "web.config" filen (ved at bruge Visual Studio eller dit favoritprogram), scroll til bunden, tilføj forbindelsesstrengen til din database i feltet som hedder "umbracoDbDSN" og gem filen.

Klik på Forsøg igen knappen når du er færdig.
Mere information om at redigere web.config her.

]]>
+

For at fortsætte bedes du venligst rette "web.config" filen (ved at bruge Visual Studio eller dit favoritprogram), scroll til bunden, tilføj forbindelsesstrengen til din database i feltet som hedder "umbracoDbDSN" og gem filen.

Klik på Forsøg igen knappen når du er færdig.
Mere information om at redigere web.config her.

]]> Kontakt venligst din ISP hvis det er nødvendigt. Hvis du installerer på en lokal maskine eller server kan du muligvis få informationerne fra din systemadministrator.]]> Tryk på Opgradér knappen for at opgradere din database til Umbraco %0%

Bare rolig - intet indhold vil blive slettet og alt vil stadig fungere bagefter!

]]>
Tryk på Næste for at fortsætte.]]> @@ -828,7 +888,7 @@ Sætter folderrettigheder op Umbraco har behov for 'write/modify' adgang til bestemte foldere, for at kunne gemme filer som billeder og PDF'er. Umbraco gemmer også midlertidige data (eksempelvis cachen) for at forbedre ydelsen på dit website. Jeg har lyst til at begynde på bar bund - lær hvordan) Du kan stadig vælge at installere Runway senere. Gå venligst til Udvikler-sektionen og vælg Pakker.]]> + lær hvordan) Du kan stadig vælge at installere Runway senere. Gå venligst til Udvikler-sektionen og vælg Pakker.]]> Du har lige opsat en ren Umbraco-platform. Hvad ønsker du nu at gøre? Runway er installeret Dette er vores liste over anbefalede moduler. Kryds dem af du ønsker at installere eller se den fulde liste af moduler ]]> @@ -874,7 +934,7 @@ Log ind nedenfor Log ind med Din session er udløbet - © 2001 - %0%
umbraco.com

]]>
+ © 2001 - %0%
umbraco.com

]]>
Glemt adgangskode? En e-mail vil blive sendt til den angivne adresse med et link til at nulstille din adgangskode En e-mail med instruktioner for nulstilling af adgangskoden vil blive sendt til den angivne adresse, hvis det matcher vores optegnelser @@ -926,7 +986,7 @@ Mange hilsner fra Umbraco robotten

Dette er en automatisk mail for at informere dig om at opgaven '%1%' er blevet udførtpå siden '%2%' af brugeren '%3%'

Opdateringssammendrag:

%6%

Hav en fortsat god dag!

De bedste hilsner fra umbraco robotten

]]> +      RET       

Opdateringssammendrag:

%6%

Hav en fortsat god dag!

De bedste hilsner fra Umbraco robotten

]]> [%0%] Notificering om %1% udført på %2% Notificeringer @@ -999,6 +1059,7 @@ Mange hilsner fra Umbraco robotten Bemærk: at dokumenter og medier som afhænger af denne pakke vil muligvis holde op med at virke, så vær forsigtig. Hvis i tvivl, kontakt personen som har udviklet pakken.]]> Pakke version + Opgraderer fra version Pakke allerede installeret Denne pakke kan ikke installeres, den kræver en minimum Umbraco version af Afinstallerer... @@ -1036,17 +1097,18 @@ Mange hilsner fra Umbraco robotten Hvis du ønsker at give adgang til enkelte medlemmer + Utilstrækkelige bruger adgang til a udgive alle under dokumenter Udgivelsen kunne ikke udgives da publiceringsdato er sat - + + - - %0% kunne ikke udgives, fordi et 3. parts modul annullerede handlingen Medtag ikke-udgivede undersider @@ -1070,6 +1132,17 @@ Mange hilsner fra Umbraco robotten Du har valgt et medie som er slettet eller lagt i papirkurven Du har valgt medier som er slettede eller lagt i papirkurven Slettet + Åben i mediebiblioteket + Skift medie + Nulstil medie beskæring + Rediger %0% på %1% + Annuller indsættelse? + + Du har foretaget ændringer til bruge af dette media. Er du sikker på at du vil annullere? + Fjern? + Fjern brugen af alle medier? + Udklipsholder + Ikke tilladt indtast eksternt link @@ -1086,8 +1159,11 @@ Mange hilsner fra Umbraco robotten Tilføj ny beskæring Acceptér Fortryd + Brugerdefineret + Ændringer + Oprettet Vælg en version at sammenligne med den nuværende version Nuværende version Rød tekst vil ikke blive vist i den valgte version. Grøn betyder tilføjet]]> @@ -1143,12 +1219,14 @@ Mange hilsner fra Umbraco robotten 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 + Denne node har ingen under noder at sortere Validering Valideringsfejl skal rettes før elementet kan gemmes Fejlet Gemt + Gemt. For at se ændringerne skal du genindlæse din browser Utilstrækkelige brugerrettigheder, kunne ikke fuldføre handlingen Annulleret Handlingen blev annulleret af et 3. part tilføjelsesprogram @@ -1169,10 +1247,16 @@ Mange hilsner fra Umbraco robotten Udgivelse fejlede da overliggende side ikke er udgivet Indhold publiceret og nu synligt for besøgende + %0% dokumenter udgivet og synlige på hjemmesiden + %0% udgivet og synligt på hjemmesiden + %0% dokumenter udgivet for sprogene %1% og synlige på hjemmesiden Indhold gemt Husk at publicere for at gøre det synligt for besøgende + En planlægning for udgivelse er blevet opdateret + %0% gemt Send til Godkendelse Rettelser er blevet sendt til godkendelse + %0% rettelser er blevet sendt til godkendelse Medie gemt Medie gemt uden problemer Medlem gemt @@ -1199,6 +1283,8 @@ Mange hilsner fra Umbraco robotten Skabelon gemt Skabelon gemt uden fejl! Indhold fjernet fra udgivelse + Indhold variation %0% afpubliceret + Det krævet sprog '%0%' var afpubliceret. Alle sprog for dette indholds element er nu afpubliceret. Partial view gemt Partial view gemt uden fejl! Partial view ikke gemt @@ -1213,11 +1299,20 @@ Mange hilsner fra Umbraco robotten Brugergrupper er blevet indstillet Låste %0% brugere op %0% er nu låst op + Medlem blev exportet til fil + Der skete en fejl under exporteringen af medlemmet Brugeren %0% blev slettet Invitér bruger Invitationen blev gensendt til %0% + Kan ikke udgive dokumentet da det krævet '%0%' ikke er udgivet + Validering fejlede for sproget '%0%' Dokumenttypen blev eksporteret til en fil Der skete en fejl under eksport af en dokumenttype + Udgivelses datoen kan ikke ligge i fortiden + Kan ikke planlægge dokumentes udgivelse da det krævet '%0%' ikke er udgivet + Kan ikke planlægge dokumentes udgivelse da det krævet '%0%' har en senere udgivelses dato end et ikke krævet sprog + Afpubliceringsdatoen kan ikke ligge i fortiden + Afpubliceringsdatoen kan ikke være før udgivelsesdatoen Tilføj style @@ -1283,11 +1378,12 @@ Mange hilsner fra Umbraco robotten ]]> Sektionsnavn Sektionen er obligatorisk - + @section -definition. - + ]]> Query builder sider returneret, på + Kopier til udkilpsholder Returner alt indhold indhold af typen "%0%" @@ -1336,11 +1432,14 @@ Mange hilsner fra Umbraco robotten Grid layout Et layout er det overordnede arbejdsområde til dit grid - du vil typisk kun behøve ét eller to Tilføj grid layout + Rediger grid layout 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 + Rediger rækkekonfiguration Juster rækken ved at indstille cellebredder og tilføje yderligere celler + Ingen yderligere konfiguration tilgængelig Kolonner Det totale antal kolonner i dit grid Indstillinger @@ -1353,6 +1452,9 @@ Mange hilsner fra Umbraco robotten Vælg ekstra Vælg standard er tilføjet + Advarsel + Du sletter en rækkekonfiguration + Sletning af et rækkekonfigurations navn vil resultere i et tab af data for alle eksiterende indhold som bruger dens konfiguration. Maksimalt emner Efterlad blank eller sæt til 0 for ubegrænset @@ -1377,9 +1479,11 @@ Mange hilsner fra Umbraco robotten Indholdstypen bliver brugt i en komposition og kan derfor ikke blive anvendt som komposition Der er ingen indholdstyper tilgængelige at bruge som komposition Når du fjerner en komposition vil alle associerede indholdsdata blive slettet. Når først dokumenttypen er gemt, er der ingen vej tilbage. - Tilgængelige editors + Opret ny indstilling Genbrug - Editor indstillinger + Input indstillinger + Tilgængelige indstillinger + Opret ny indstilling Konfiguration Ja, slet blev flyttet til @@ -1405,14 +1509,24 @@ Mange hilsner fra Umbraco robotten fane har ingen sorteringsrækkefølge Hvor er denne komposition brugt? Denne komposition brugt i kompositionen af de følgende indholdstyper: - Tillad sprogvariation + Tillad variationer + Tillad sprogvariation + Tillad segmentering + Tillader sprogvariationer + Tillader segmentering Tillad at redaktører kan oprette indhold af denne type på flere sprog. + Tillad at redaktører kan oprette dette indhold på flere sprog. + Tillad at redaktører kan oprette flere udgaver af denne type indhold. Tillad sprogvariation + Tillad segmentering Element-type Er en Element-type En Element-type er tiltænkt brug i f.eks. Nested Content, ikke i indholdstræet. + En Dokumenttype kan ikke ændres til en Element-type efter den er blevet brugt til at oprette en eller flere indholds elementer. Dette benyttes ikke for en Element-type Du har lavet ændringer til denne egenskab. Er du sikker på at du vil kassere dem? + Visning + Label hen over (fuld bredde) Tilføj sprog @@ -1452,27 +1566,33 @@ Mange hilsner fra Umbraco robotten Casing Kodning Felt som skal indsættes - Konvertér linieskift - Erstatter et linieskift med html-tag'et &lt;br&gt; + Konvertér linjeskift + Ja, konverter linjeskift + Erstatter et linjeskift med html-tag'et &lt;br&gt; Custom felter Ja, kun dato Format og kodning Formatér som dato + Formater værdien som en dato eller en dato med tid, i forhold til den aktive kultur HTML indkod Vil erstatte specielle karakterer med deres HTML jævnbyrdige. Denne tekst vil blive sat ind lige efter værdien af feltet Denne tekst vil blive sat ind lige før værdien af feltet Lowercase + Ændre udskrift Ingen + Udskrift eksempel Indsæt efter felt Indsæt før felt Rekursivt + Ja, lav det rekursivt + Separator Fjern paragraf-tags Fjerner eventuelle &lt;P&gt; omkring teksten Standard felter Store bogstaver URL encode - Hvis indholdet af felterne skal sendes til en url, skal denne slåes til så specialtegn formateres + Hvis indholdet af felterne skal sendes til en URL, skal denne slåes til så specialtegn formateres Denne tekst bruges hvis ovenstående felter er tomme Dette felt vil blive brugt hvis ovenstående felt er tomt Ja, med klokkeslæt. Dato/tid separator: @@ -1552,10 +1672,12 @@ Mange hilsner fra Umbraco robotten Skift dit kodeord Skift billede Nyt kodeord + Minium %0% karakterer tilbage! + Der skal som minium være %0% specielle karakterer. 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' + Du kan ændre dit kodeord, som giver dig adgang til Umbraco backoffice ved at udfylde formularen og klikke på knappen 'Skift dit kodeord' Indholdskanal Opret endnu en bruger 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. @@ -1586,8 +1708,10 @@ Mange hilsner fra Umbraco robotten Adgangskode Nulstil kodeord Dit kodeord er blevet ændret! + Kodeord ændret Bekræft venligst dit nye kodeord Indtast dit nye kodeord + Dit nye kodeord kan ikke være blankt! Nuværende kodeord ugyldig nuværende kodeord Dit nye kodeord må ikke være tomt! @@ -1607,6 +1731,7 @@ Mange hilsner fra Umbraco robotten Vælg brugergrupper Ingen startnode valgt Ingen startnoder valgt + Indhold startnode Begræns indholdstræet til en bestemt startnode Indhold startnoder Begræns indholdstræet til bestemte startnoder @@ -1620,7 +1745,7 @@ Mange hilsner fra Umbraco robotten 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. - Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen. + Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen. Hvis du uploader et billede af dig selv, gør du det nemt for andre brugere at genkende dig. Klik på cirklen ovenfor for at uploade et billede. Forfatter Skift @@ -1632,7 +1757,11 @@ Mange hilsner fra Umbraco robotten 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%

]]>
+

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%

]]>
+ Inviter + Gensender invitation... + Slet bruger + Er du sikker på du ønsker at slette denne brugers konto? Alle Aktiv Deaktiveret @@ -1644,30 +1773,44 @@ Mange hilsner fra Umbraco robotten Nyeste Ældste Sidst logget ind + Ingen brugere er blevet tilføjet Validering Valider som e-mail Valider som tal - Valider som Url + Valider som URL ...eller indtast din egen validering Feltet er påkrævet + Indtast en selvvalgt validerings fejlbesked (valgfrit) Indtast et regulært udtryk + Indtast en selvvalgt validerings fejlbesked (valgfrit) Du skal tilføje mindst Du kan kun have + Tilføj op til elementer + URL(er) + URL(er) valgt elementer valgt Ugyldig dato Ikke et tal + Ikke en gyldig numerisk trinstørrelse Ugyldig e-mail - %1% more.]]> - %1% too many.]]> + Værdien kan ikke være tom + Værdien kan ikke være tom + Værdien er ugyldig, som ikke matcher det korrekte format + Selvvalgt validering + %1% mere.]]> + %1% for mange.]]> Slå URL tracker fra Slå URL tracker til + Kultur Original URL Viderestillet til + Viderestil URL håndtering + De følgende URLs viderestiller til dette indholds element Der er ikke lavet nogen viderestillinger Når en udgivet side bliver omdøbt eller flyttet, vil en viderestilling automatisk blive lavet til den nye side. Er du sikker på at du vil fjerne viderestillingen fra '%0%' til '%1%'? @@ -1736,12 +1879,41 @@ Mange hilsner fra Umbraco robotten Åben backoffice søgning Åben/Luk backoffice hjælp Åben/Luk dine profil indstillinger + Tilføj domæne på %0% + Opret ny node under %0% + Opsæt offentlig adgang på %0% + Opsæt rettigheder på %0% + Juster soterings rækkefølgen for %0% + Opret indholds skabelon baseret på %0% + Åben kontext menu for Aktivt sprog Skift sprog til Opret ny mappe + Delvist View + Delvist View Macro + Medlem + Data type + Søg i viderestillings dashboardet + Søg i brugergruppe sektionen + Søg i bruger sektionen Opret element + Opret Rediger Navn + Tilføj ny række + Vis flere muligheder + Søg i Umbraco backoffice + Søg efter indholdsnoder, medienoder osv. i backoffice + Når autoudfyldnings resultaterne er klar, tryk op og ned pilene, eller benyt tab knappen og brug enter knappen til at vælge. + Vej + Fundet i + Har oversættelse + Mangler oversættelse + Ordbogs elementer + Udfør handling %0% på %1% noden + Tilføj billede overskrift + Søg i indholdstræet + Maximum antal Referencer @@ -1753,10 +1925,19 @@ Mange hilsner fra Umbraco robotten Brugt i Medlems Typer Ingen referencer til Medlems Typer. Brugt af + Brugt i Dokumenter + Brugt i Medlemmer + Brugt i Medier + Slet gemte søgning Log type + Vælg alle + Fravælg alle Gemte søgninger + Gem søgning + Indtast et navn for din søgebetingelse + Filter søgning Samlet resultat Dato Type @@ -1781,13 +1962,102 @@ Mange hilsner fra Umbraco robotten Find logs med Namespace Find logs med maskin navn Åben + Henter + Hver 2 sekunder + Hver 5 sekunder + Hver 10 sekunder + Hver 20 sekunder + Hver 30 sekunder + Henter hver 2s + Henter hver 5s + Henter hver 10s + Henter hver 20s + Henter hver 30s Kopier %0% %0% fra %1% + Samling af %0% Fjern alle elementer + Ryd udklipsholder Åben egenskabshandlinger + Luk egenskabshandlinger + + + Vælg elementtype + Tilføj en indstillings elementtype + Tilføj visning + Tilføj stylesheet + Vælg billede + Opret ny elementtype + Overskriv stylesheet + Tilføj stylesheet + Redigerings udseende + Data modeller + katalog udseende + Baggrunds farve + Ikon farve + Indholds model + Label + Speciel visning + Vis speciel visning beskrivelsen + Overskrift hvordan denne block præsenteres i backoffice interfacet. Vælg en .html fil der indeholder din præsensation. + Indstillings model + Rederings lagets størrelse + Tilføj speciel visning + Tilføj instillinger + Overskriv label form + %0%?]]> + %0%?]]> + Indholdet vil stadigt eksistere, men redigering af dette indhold vil ikke være muligt. Indholdet vil blive vist som ikke understøttet indhold. + + Billede + Tilføj billede + Opret ny + Udklipsholder + Indstillinger + Avanceret + Skjuld indholds editoren + Du har lavet ændringer til dette indhold. Er du sikker på at du vil kassere dem? + Annuller oprettelse? + + Error! + The ElementType of this block does not exist anymore + Tilføj indhold + Tilføj %0% + Feltet %0% bruger editor %1% som ikke er supporteret for blokke. + + + Hvad er Indholdsskabeloner? + Indholdsskabeloner er foruddefineret indhold der kan vælges når der oprettes nye indholdselementer. + Hvordan opretter jeg en Indholdsskabelon? + + Der er to måder at oprette Indholdsskabeloner på:

+
    +
  • Højreklik på en indholdsnode og vælg "Opret indholdsskabelon" for at oprette en ny Indholdsskabelon.
  • +
  • Højreklik på Indholdsskabeloner i sektionen Indstillinger og vælg den dokumenttype du vil oprette en Indholdsskabelon for.
  • +
+

Når indholdsskabelonen har fået et navn, kan redaktører begynde at bruge indholdsskabelonen som udgangspunkt for deres nye side.

+ ]]> +
+ Hvordan vedligeholder jeg Indholdsskabeloner? + Du kan redigere og slette Indholdsskabeloner fra "Indholdsskabeloner" i sektionen Indstillinger. Fold dokumenttypen som Indholdsskabelonen er baseret på ud og klik på den for at redigere eller slette den. + + + Afslut + Afslut forhåndsvisning + Vis i nyt vindue + Åben forhåndsvisning i nyt vindue + Forhåndsvisning af indholdet? + Du har afslutet forhåndsvisning, vil du starte forhåndsvisning igen for at se seneste gemte version af indholdet? + Start forhåndsvisning + Se udgivet indhold + Se udgivet indhold? + Du er i forhåndsvisning, vil du afslutte for at se den udgivet version? + Se udgivet version + Forbliv i forhåndsvisning diff --git a/TestSite/Umbraco/Config/Lang/de.xml b/TestSite/Umbraco/Config/Lang/de.xml index e45d97e..dab751f 100644 --- a/TestSite/Umbraco/Config/Lang/de.xml +++ b/TestSite/Umbraco/Config/Lang/de.xml @@ -4,10 +4,6 @@ The Umbraco community https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - - - - Kulturen und Hostnamen Protokoll @@ -140,7 +136,7 @@ Speichern und zur Abnahme übergeben Listenansicht sichern Veröffentlichung planen - Vorschau + Vorschau Die Vorschaufunktion ist deaktiviert, da keine Vorlage zugewiesen ist Stil auswählen Stil anzeigen @@ -185,26 +181,6 @@ Sortieren Verlauf (alle Variationen) - - Um den Typ des ausgewählten Dokuments zu ändern, wählen Sie bitte zunächst aus der Liste der an dieser Stelle erlaubten Dokumenttypen. - Im Anschluss bestätigen oder korrigieren Sie die Zuordnung der Eigenschaften und klicken Sie auf 'Speichern'. - Der Inhalt wurde neu veröffentlicht. - Derzeitige Eigenschaft - Derzeitiger Datentyp - Der Typ dieses Dokuments kann nicht geändert werden, da an dieser Stelle keine Alternativen zugelassen sind. Ein alternativer Dokumenttyp kann nur dann verwendet werden, wenn er unterhalb des diesem Dokument übergeordneten Elements angelegt werden darf. - Dokumenttyp geändert - Eigenschaften zuordnen - Dieser Eigenschaft zuordnen - Neue Vorlage - Neuer Typ - keiner - Inhalt - Neuen Dokumenttyp auswählen - Der Typ des ausgewählten Dokuments wurde erfolgreich zu [new type] geändert und die Eigenschaften wie folgend zugeordnet: - nach - Die Zuordnung der Eigenschaften kann nicht abgeschlossen werden, da mindestens eine Eigenschaft mehrfach zugeordnet werden soll. - Nur an dieser Stelle erlaubte Dokumenttypen werden angezeigt. - Es konnte kein Verzeichnis unter dem Knoten mit der ID %0% angelegt werden. Es konnte kein Verzeichnis unter dem Knoten mit dem Namen %0% angelegt werden. @@ -288,7 +264,7 @@ Dies führt zur folgenden Zeit auf dem Server: Was bedeutet dies? + Was bedeutet dies? ]]> Wollen Sie dieses Element wirklich entfernen? @@ -449,8 +425,8 @@ Der Papierkorb wird geleert. Bitte warten Sie und schließen Sie das Fenster erst, wenn der Vorgang abgeschlossen ist. Der Papierkorb ist leer Wenn Sie den Papierkorb leeren, werden die enthaltenen Elemente endgültig gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden. - Der Webservice von <a target='_blank' href='http://regexlib.com'>regexlib.com</a> ist zur Zeit nicht erreichbar. Bitte versuchen Sie es später erneut. - Finden Sie einen vorbereiteten regulären Ausdruck zur Validierung der Werte, die in dieses Feld eingegeben werden - zum Beispiel 'email, 'plz', 'url' oder ähnlich. + Der Webservice von <a target='_blank' rel='noopener' href='http://regexlib.com'>regexlib.com</a> ist zur Zeit nicht erreichbar. Bitte versuchen Sie es später erneut. + Finden Sie einen vorbereiteten regulären Ausdruck zur Validierung der Werte, die in dieses Feld eingegeben werden - zum Beispiel 'email, 'plz', 'URL' oder ähnlich. Macro entfernen Pflichtfeld Die Website-Index wurd neu erstellt @@ -713,7 +689,6 @@ Aus Ok Öffnen - Optionen An oder @@ -825,9 +800,11 @@ Die Datenbank wurde für Umbraco %0% konfiguriert. Klicken Sie auf <strong>weiter</strong>, um fortzufahren. - <p>Die angegebene Datenbank ist leider nicht erreichbar. Bitte prüfen Sie die Verbindungszeichenfolge ("Connection String") in der "web.config"-Datei.</p> - <p>Um fortzufahren, passen Sie bitte die "web.config"-Datei mit einem beliebigen Text-Editor an. Scrollen Sie dazu nach unten, fügen Sie die Verbindungszeichenfolge für die zuverbindende Datenbank als Eintrag "UmbracoDbDSN" hinzu und speichern Sie die Datei.</p> - <p>Klicken Sie nach erfolgter Anpassung auf <strong>Wiederholen</strong>.<br />Wenn Sie weitere technische Informationen benötigen, besuchen Sie <a href="https://our.umbraco.com/wiki" target="_blank">The Umbraco documentation wiki</a>.</p> + Die angegebene Datenbank ist leider nicht erreichbar. Bitte prüfen Sie die Verbindungszeichenfolge ("Connection String") in der "web.config"-Datei.

+

Um fortzufahren, passen Sie bitte die "web.config"-Datei mit einem beliebigen Text-Editor an. Scrollen Sie dazu nach unten, fügen Sie die Verbindungszeichenfolge für die zuverbindende Datenbank als Eintrag "UmbracoDbDSN" hinzu und speichern Sie die Datei.

+

Klicken Sie nach erfolgter Anpassung auf Wiederholen.
Wenn Sie weitere technische Informationen benötigen, besuchen Sie The Umbraco documentation wik.

+ ]]>
Um diesen Schritt abzuschließen, müssen Sie die notwendigen Informationen zur Datenbankverbindung angeben.<br />Bitte kontaktieren Sie Ihren Provider bzw. Server-Administrator für weitere Informationen. @@ -869,7 +846,7 @@ Ich möchte mit einem leeren System ohne Inhalte und Vorgaben starten Die Website ist zur Zeit komplett leer und ohne Inhalte und Vorgaben zu Erstellung eigener Dokumenttypen und Vorlagen bereit. - (<a href="http://Umbraco.tv/documentation/videos/for-site-builders/foundation/document-types">So geht's</a>) + (<a href="https://umbraco.tv/documentation/videos/for-site-builders/foundation/document-types">So geht's</a>) Sie können "Runway" auch jederzeit später installieren. Verwenden Sie hierzu den Punkt "Pakete" im Entwickler-Bereich. Die Einrichtung von Umbraco ist abgeschlossen und das Content-Management-System steht bereit. Wie soll es weitergehen? @@ -930,7 +907,7 @@ Hier anmelden: Anmelden mit Sitzung abgelaufen - <p style="text-align:right;">&copy; 2001 - %0% <br /><a href="http://umbraco.com" style="text-decoration: none" target="_blank">umbraco.org</a></p> + <p style="text-align:right;">&copy; 2001 - %0% <br /><a href="https://umbraco.com" style="text-decoration: none" target="_blank" rel="noopener">umbraco.org</a></p> Kennwort vergessen? Es wird eine E-Mail mit einem Kennwort-Zurücksetzen-Link an die angegebene Adresse geschickt. Es wird eine E-Mail mit Anweisungen zum Zurücksetzen des Kennwortes an die angegebene Adresse geschickt sofern diese im Datenbestand gefunden wurde. @@ -989,7 +966,7 @@ - + Klicken Sie hier, um Ihr Kennwort zurück zu setzen @@ -1107,7 +1084,7 @@
- Bearbeiten
+ Bearbeiten @@ -1173,7 +1150,7 @@ Installiert Installierte Pakete Lokale Installation - Benenden + Abschließen Diese Paket hat keine Einstellungen Es wurden noche keine Pakete angelegt Sie haben keine Pakete installiert @@ -1391,7 +1368,6 @@ Registerkarten Masterdokumenttyp aktiviert Dieser Dokumenttyp verwendet - als Masterdokumenttyp. Register vom Masterdokumenttyp werden nicht angezeigt und können nur im Masterdokumenttyp selbst bearbeitet werden Für dieses Register sind keine Eigenschaften definiert. Klicken Sie oben auf "neue Eigenschaft hinzufügen", um eine neue Eigenschaft hinzuzufügen. Zugehörige Vorlage anlegen Bildsymbol hinzufügen @@ -1496,9 +1472,6 @@ Neuer Stil Stil bearbeiten Rich text editor Stile - - Definiere die Stile, die im 'Rich-Text-Editor' verfügbar sein sollen. - Definiere die Styles, die im Rich-Text-Editor dieses Stylesheets verfügbar sein sollen. Stylesheet bearbeiten Stylesheet-Regel bearbeiten @@ -1564,10 +1537,10 @@
Bereichsname Bereich ist notwendig - + @section Definition gleichen Namens enthalten, anderfalls tritt ein Fehler auf. - + ]]> Abfrage-Generator zurückgegebene Elemente, in Ich möchte @@ -2012,7 +1985,7 @@ - + Um diese Einladung anzunehmen,
klicken Sie diese Schaltfläche
@@ -2182,7 +2155,7 @@ Kultur Original URL Weiterleiten zu - Url-Weiterleitungen verwalten + URL-Weiterleitungen verwalten Die folgenden URLs leiten auf diesen Inhalt: Es wurden keine Weiterleitungen angelegt @@ -2258,4 +2231,8 @@ Back-Office Hilfe öffnen / schliessen Ihre Profil-Einstellungen öffnen / schliessen + + Wählen Sie Alle + Alle abwählen + diff --git a/TestSite/Umbraco/Config/Lang/en.xml b/TestSite/Umbraco/Config/Lang/en.xml index 85b23f5..602a8c6 100644 --- a/TestSite/Umbraco/Config/Lang/en.xml +++ b/TestSite/Umbraco/Config/Lang/en.xml @@ -16,6 +16,7 @@ Create group Delete Disable + Edit settings Empty recycle bin Enable Export Document Type @@ -25,11 +26,12 @@ Exit Move Notifications - Public access + Restrict Public Access Publish Unpublish Reload Republish entire site + Remove Rename Restore Set permissions for the page %0% @@ -64,12 +66,12 @@ 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 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 set and change access restrictions for a node Allow access to publish a node Allow access to unpublish a node Allow access to change permissions for a node @@ -142,6 +144,7 @@ Save list view Schedule Preview + Save and preview Preview is disabled because there's no template assigned Choose style Show styles @@ -153,6 +156,8 @@ Cancel Confirm More publishing options + Submit + Submit and close Viewing for @@ -168,6 +173,7 @@ Content sent for publishing Content sent for publishing for languages: %0% Sort child items performed by user + %0% Copy Publish Publish @@ -180,28 +186,9 @@ Send To Publish Send To Publish Sort + Custom History (all variants) - - 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. - Failed to create a folder under parent with ID %0% Failed to create a folder under parent with name %0% @@ -243,15 +230,15 @@ This document is published but is not visible because the parent '%0%' is unpublished This culture is published but is not visible because it is unpublished on parent '%0%' 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% - This document is published but its url cannot be routed + Could not get the URL + This document is published but its URL would collide with content %0% + This document is published but its URL cannot be routed Publish Published Published (pending changes) Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + %0% and all content items underneath and thereby making their content publicly available.]]> + Publish at Unpublish at Clear Date @@ -261,6 +248,7 @@ Statistics Title (optional) Alternative text (optional) + Caption (optional) Type Unpublish Unpublished @@ -275,20 +263,20 @@ Child items Target This translates to the following time on the server: - What does this mean?]]> + 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. Are you sure you want to delete all items? - No content types are configured for this property. - Add element type - Select element type - Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. + No Content Types are configured for this property. + Add Element Type + Select Element Type + Select the group whose properties should be displayed. If left blank, the first group on the Element Type will be used. Enter an angular expression to evaluate against each item for its name. Use to display the item index Add another text box Remove this text box Content root - Include drafts: also publish unpublished content items. + Include unpublished content items. This value is hidden. If you need access to view this value please contact your website administrator. This value is hidden. What languages would you like to publish? All languages with content are saved! @@ -302,8 +290,19 @@ Unpublished Languages Unmodified Languages These languages haven't been created - Ready to Publish? + + All new variants will be saved. + Which variants would you like to publish? + Choose which variants to be saved. + Pick variants to send for approval. + Set scheduled publishing... + Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. + The following variants is required for publishing to take place: + + We are not ready to Publish + Ready to publish? Ready to Save? + Reset focal point Send for approval Select the date and time to publish and/or unpublish the content item. Create new @@ -337,24 +336,46 @@ All Members Member groups have no additional properties for editing. + + Failed to copy content type + Failed to move content type + + + Failed to copy media type + Failed to move media type + Auto pick + + + Failed to copy member type + 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 + Select the Document Type you want to make a content template for Enter a folder name Choose a type and a title - Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - Document Types within the Settings section.]]> + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> The selected page in the content tree doesn't allow for any pages to be created below it. - Edit permissions for this document type - Create a new document type - Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> - Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Edit permissions for this Document Type + Create a new Document Type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> + Media Types within the Settings section, by editing the Allowed child node types under Permissions.]]> The selected media in the tree doesn't allow for any other media to be created below it. - Edit permissions for this media type + Edit permissions for this Media Types Document Type without a template + Document Type with Template + The data definition for a content page that can be created by editors in the content tree and is directly accessible via a URL. + Document Type + The data definition for a content component that can be created by editors in the content tree and be picked on other pages but has no direct URL. + Element Type + Defines the schema for a repeating set of properties, for example, in a 'Block List' or 'Nested Content' property editor. + Composition + Defines a re-usable set of properties that can be included in the definition of multiple other Document Types. For example, a set of 'Common Page Settings'. + Folder + Used to organise the Document Types, Compositions and Element Types created in this Document Type tree. New folder - New data type + New Data Type New JavaScript file New empty partial view New partial view macro @@ -414,7 +435,11 @@ Manage hostnames Close this window Are you sure you want to delete + Are you sure you want to delete %0% based on %1% Are you sure you want to disable + Are you sure you want to remove + %0%]]> + %0%]]> Are you sure? Are you sure? Cut @@ -445,8 +470,8 @@ 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' + 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 @@ -478,6 +503,7 @@ Select member type Select node Select sections + Select user Select users No icons were found There are no parameters for this macro @@ -492,6 +518,9 @@ Select editor Select snippet This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. + %0%.]]> + %0% from the %1% group]]> + Yes, remove There are no dictionary items. @@ -530,7 +559,7 @@ Tools to manage the index fields The index cannot be read and will need to be rebuilt - The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation + The process is taking longer than expected, check the Umbraco log to see if there have been any errors during this operation This index cannot be rebuilt because it has no assigned IIndexPopulator @@ -561,7 +590,7 @@ Create custom list view Remove custom list view - A content type, media type or member type with this alias already exists + A Content Type, Media Type or Member Type with this alias already exists Renamed @@ -581,9 +610,9 @@ 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 + 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 + and all property types & property data using this Data Type Select the folder to move to in the tree structure below was moved underneath @@ -636,6 +665,7 @@ Clear Close Close Window + Close Pane Comment Confirm Constrain @@ -653,6 +683,7 @@ Design Dictionary Dimensions + Discard Down Download Edit @@ -675,6 +706,7 @@ Id Import Include subfolders in search + Search only this folder Info Inner margin Insert @@ -737,6 +769,7 @@ Sort Status Submit + Success Type Type to search... under @@ -744,7 +777,7 @@ Update Upgrade Upload - Url + URL User Username Value @@ -767,8 +800,8 @@ Other Articles Videos - Clear Installing + Avatar for Blue @@ -795,6 +828,7 @@ General Editor Toggle allow culture variants + Toggle allow segmentation Background colour @@ -821,8 +855,8 @@

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.

]]>
+ done.
+ 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.]]> @@ -868,8 +902,8 @@ ]]> I want to start from scratch learn how) + Your website is completely empty at the moment, so that's perfect if you want to start from scratch and create your own Document Types and templates. + (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? @@ -882,7 +916,7 @@ 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, + "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. @@ -909,7 +943,7 @@ Get help from our award winning community, browse the documentation or watch som 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]]> +To manage your website, simply open the Umbraco backoffice and start adding content, updating the templates and stylesheets or add new functionality]]>
Connection to database failed. Umbraco Version 3 Umbraco Version 4 @@ -937,7 +971,7 @@ To manage your website, simply open the Umbraco back office and start adding con Log in below Sign in with Session timed out - © 2001 - %0%
Umbraco.com

]]>
+ © 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 @@ -988,14 +1022,14 @@ To manage your website, simply open the Umbraco back office and start adding con Password reset requested

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

@@ -1116,7 +1150,7 @@ To manage your website, simply open the Umbraco back office and start adding con
- + Click this link to reset your password
- EDIT
+ EDIT
@@ -1235,6 +1269,7 @@ To manage your website, simply open the Umbraco back office and start adding con All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... + Verified to work on Umbraco Cloud Paste with full formatting (Not recommended) @@ -1301,6 +1336,17 @@ To manage your website, simply open the Umbraco back office and start adding con 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 Trashed + Open in Media Library + Change Media Item + Reset media crop + Edit %0% on %1% + Discard creation? + + You have made changes to this content. Are you sure you want to discard them? + Remove? + Remove all medias? + Clipboard + Not allowed enter external link @@ -1317,12 +1363,15 @@ To manage your website, simply open the Umbraco back office and start adding con Add new crop Done Undo edits + User defined - Select a version to compare with the current version + Changes + Created Current version Red text will not be shown in the selected version. , green means added]]> Document has been rolled back + Select a version to compare with the current version 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 @@ -1356,7 +1405,7 @@ To manage your website, simply open the Umbraco back office and start adding con Default template - 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) + 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 @@ -1367,7 +1416,6 @@ To manage your website, simply open the Umbraco back office and start adding con 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. Create matching template Add icon @@ -1453,8 +1501,8 @@ To manage your website, simply open the Umbraco back office and start adding con User %0% was deleted Invite user Invitation has been re-sent to %0% - Document type was exported to file - An error occurred while exporting the document type + Document Type was exported to file + An error occurred while exporting the Document Type Add style @@ -1516,9 +1564,9 @@ To manage your website, simply open the Umbraco back office and start adding con ]]> Section Name Section is mandatory - + @section definition, otherwise an error is shown. - + ]]> Query builder items returned, in copy to clipboard @@ -1565,16 +1613,20 @@ To manage your website, simply open the Umbraco back office and start adding con This content is allowed here Click to embed Click to insert image + Click to insert macro 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 + Edit Grid Layout Adjust the layout by setting column widths and adding additional sections Row configurations Rows are predefined cells arranged horizontally Add row configuration + Edit row configuration Adjust the row by setting cell widths and adding additional cells + No further configuration available Columns Total combined number of columns in the grid layout Settings @@ -1612,10 +1664,10 @@ To manage your website, simply open the Umbraco back office and start adding con 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. - Removing a composition will delete all the associated property data. Once you save the document type there's no way back. + 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. + Removing a composition will delete all the associated property data. Once you save the Document Type there's no way back. Create new Use existing Editor settings @@ -1626,12 +1678,12 @@ To manage your website, simply open the Umbraco back office and start adding con Select the folder to move Select the folder to copy to in the tree structure below - All Document types + 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 + 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 @@ -1644,15 +1696,24 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order Where is this composition used? This composition is currently used in the composition of the following content types: - Allow varying by culture + Allow variations + Allow vary by culture + Allow segmentation + Vary by culture + Vary by segments Allow editors to create content of this type in different languages. + Allow editors to create content of different languages. + Allow editors to create segments of this content. Allow varying by culture - Element type - Is an Element type - An Element type is meant to be used for instance in Nested Content, and not in the tree. - A document type cannot be changed to an Element type once it has been used to create one or more content items. - This is not applicable for an Element type + Allow segmentation + Element Type + Is an Element Type + An Element Type is meant to be used for instance in Nested Content, and not in the tree. + A document Type cannot be changed to an Element Type once it has been used to create one or more content items. + This is not applicable for an Element Type You have made changes to this property. Are you sure you want to discard them? + Appearance + Label above (full-width) Add language @@ -1806,13 +1867,15 @@ To manage your website, simply open the Umbraco back office and start adding con Administrator Category field User created - Change Your Password + Change your password Change photo New password + Minimum %0% character(s) to go! + There should be at least %0% special character(s) in there. hasn't been locked out The password hasn't been changed 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 + You can change your password for accessing the Umbraco backoffice by filling out the form below and click the 'Change Password' button Content Channel Create another user Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. @@ -1843,6 +1906,7 @@ To manage your website, simply open the Umbraco back office and start adding con Password Reset password Your password has been changed! + Password changed Please confirm the new password Enter your new password Your new password cannot be blank! @@ -1944,7 +2008,7 @@ To manage your website, simply open the Umbraco back office and start adding con - + Click this link to accept the invite @@ -2010,11 +2074,18 @@ To manage your website, simply open the Umbraco back office and start adding con Enter a custom validation error message (optional) You need to add at least You can only have + Add up to items + URL(s) + URL(s) selected items selected Invalid date Not a number + Not a valid numeric step size Invalid email + Value cannot be null + Value cannot be empty + Value is invalid, it does not match the correct pattern Custom validation %1% more.]]> %1% too many.]]> @@ -2126,7 +2197,7 @@ To manage your website, simply open the Umbraco back office and start adding con The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. - You can add your own health checks, have a look at the documentation for more information about custom health checks.

+ You can add your own health checks, have a look at the documentation for more information about custom health checks.

]]> @@ -2135,7 +2206,7 @@ To manage your website, simply open the Umbraco back office and start adding con Enable URL tracker Original URL Redirected To - Redirect Url Management + Redirect URL Management The following URLs redirect to this content item: No redirects have been made When a published page gets renamed or moved a redirect will automatically be made to the new page. @@ -2174,7 +2245,7 @@ To manage your website, simply open the Umbraco back office and start adding con Created Comment Name - No relations for this relation type. + No relations for this Relation Type Relation Type Relations @@ -2205,6 +2276,12 @@ To manage your website, simply open the Umbraco back office and start adding con Open backoffice search Open/Close backoffice help Open/Close your profile options + Setup Culture and Hostnames for %0% + Create new node under %0% + Setup access restrictions on %0% + Setup Permissions on %0% + Change sort order for %0% + Create Content Template based on %0% Open context menu for Current language Switch language to @@ -2212,7 +2289,7 @@ To manage your website, simply open the Umbraco back office and start adding con Partial View Partial View Macro Member - Data type + Data Type Search the redirect dashboard Search the user group section Search the users section @@ -2220,6 +2297,21 @@ To manage your website, simply open the Umbraco back office and start adding con Create Edit Name + Add new row + View more options + Search the Umbraco backoffice + Search for content nodes, media nodes etc. across the backoffice. + When autocomplete results are available, press up and down arrows, or use the tab key and use the enter key to select. + Path: + Found in + Has translation + Missing translation + Dictionary items + Select one of the options to edit the node. + Perform action %0% on the %1% node + Add image caption + Search content tree + Maximum amount References @@ -2236,40 +2328,60 @@ To manage your website, simply open the Umbraco back office and start adding con Used in Media - Log Levels - Saved Searches - Total Items - Timestamp - Level - Machine - Message - Exception - Properties - Search With Google - Search this message with Google - Search With Bing - Search this message with Bing - Search Our Umbraco - Search this message on Our Umbraco forums and docs - Search Our Umbraco with Google - Search Our Umbraco forums using Google - Search Umbraco Source - Search within Umbraco source code on Github - Search Umbraco Issues - Search Umbraco Issues on Github - Delete this search - Find Logs with Request ID - Find Logs with Namespace - Find Logs with Machine Name - Open + Delete Saved Search + Log Levels + Select all + Deselect all + Saved Searches + Save Search + Enter a friendly name for your search query + Filter Search + Total Items + Timestamp + Level + Machine + Message + Exception + Properties + Search With Google + Search this message with Google + Search With Bing + Search this message with Bing + Search Our Umbraco + Search this message on Our Umbraco forums and docs + Search Our Umbraco with Google + Search Our Umbraco forums using Google + Search Umbraco Source + Search within Umbraco source code on Github + Search Umbraco Issues + Search Umbraco Issues on Github + Delete this search + Find Logs with Request ID + Find Logs with Namespace + Find Logs with Machine Name + Open + Polling + Every 2 seconds + Every 5 seconds + Every 10 seconds + Every 20 seconds + Every 30 seconds + Polling every 2s + Polling every 5s + Polling every 10s + Polling every 20s + Polling every 30s Copy %0% %0% from %1% + Collection of %0% Remove all items + Clear clipboard Open Property Actions + Close Property Actions Wait @@ -2347,7 +2459,7 @@ To manage your website, simply open the Umbraco back office and start adding con Hours of Umbraco training videos are only a click away Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

+

Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

]]>
To get you started @@ -2358,36 +2470,115 @@ To manage your website, simply open the Umbraco back office and start adding con Find out more in the Documentation section of Our Umbraco + Read more about working with the items in Settings in the Documentation section of Our Umbraco ]]> Community Forum + Ask a question in the Community Forum ]]> tutorial videos (some are free, some require a subscription) + Watch our tutorial videos (some are free, some require a subscription) ]]> productivity boosting tools and commercial support + Find out about our productivity boosting tools and commercial support ]]> training and certification opportunities + Find out about real-life training and certification opportunities ]]> Welcome to The Friendly CMS Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. - + Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + Pick Element Type + Attach a settings Element Type + Select view + Select stylesheet + Choose thumbnail + Create new Element Type + Custom stylesheet + Add stylesheet + Editor apperance + Data models + Catalogue appearance + Background color + Icon color + Content model + Label + Custom view + Show custom view description + Overwrite how this block appears in the backoffice UI. Pick a .html file containing your presentation. + Settings model + Overlay editor size + Add custom view + Add settings + Overwrite label template + %0%?]]> + %0%?]]> + The content of this block will still be present, editing of this content will no longer be available and will be shown as unsupported content. + + Thumbnail + Add thumbnail + Create empty + Clipboard + Settings + Advanced + Force hide content editor + You have made changes to this content. Are you sure you want to discard them? + Discard creation? + + Error! + The ElementType of this block does not exist anymore + Add content + Add %0% + Property '%0%' uses editor '%1%' which is not supported in blocks. + + + What are Content Templates? + Content Templates are pre-defined content that can be selected when creating a new content node. + How do I create a Content Template? + + There are two ways to create a Content Template:

+
    +
  • Right-click a content node and select "Create Content Template" to create a new Content Template.
  • +
  • Right-click the Content Templates tree in the Settings section and select the Document Type you want to create a Content Template for.
  • +
+

Once given a name, editors can start using the Content Template as a foundation for their new page.

+ ]]> +
+ How do I manage Content Templates? + You can edit and delete Content Templates from the "Content Templates" tree in the Settings section. Expand the Document Type which the Content Template is based on and click it to edit or delete it. + + + End + End preview mode + Preview website + Open website in preview mode + Preview website? + You have ended preview mode, do you want to enable it again to view the latest saved version of your website? + Preview latest version + View published version + View published version? + You are in Preview Mode, do you want exit in order to view the published version of your website? + View published version + Stay in preview mode + + + item returned + items returned + diff --git a/TestSite/Umbraco/Config/Lang/en_us.xml b/TestSite/Umbraco/Config/Lang/en_us.xml index c6ad103..b902661 100644 --- a/TestSite/Umbraco/Config/Lang/en_us.xml +++ b/TestSite/Umbraco/Config/Lang/en_us.xml @@ -9,6 +9,7 @@ Audit Trail Browse Node Change Document Type + Change Data Type Copy Create Export @@ -16,6 +17,7 @@ Create group Delete Disable + Edit settings Empty recycle bin Enable Export Document Type @@ -25,11 +27,12 @@ Exit Move Notifications - Public access + Restrict Public Access Publish Unpublish Reload Republish entire site + Remove Rename Restore Set permissions for the page %0% @@ -64,12 +67,12 @@ 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 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 set and change access restrictions for a node Allow access to publish a node Allow access to unpublish a node Allow access to change permissions for a node @@ -140,6 +143,7 @@ Save list view Schedule Preview + Save and preview Preview is disabled because there's no template assigned Choose style Show styles @@ -153,6 +157,8 @@ Cancel Confirm More publishing options + Submit + Submit and close Viewing for @@ -169,6 +175,7 @@ Content sent for publishing Content sent for publishing for languages: %0% Sort child items performed by user + %0% Copy Publish Publish @@ -182,28 +189,9 @@ Send To Publish Send To Publish Sort + Custom History (all variants) - - 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. - Failed to create a folder under parent with ID %0% Failed to create a folder under parent with name %0% @@ -246,15 +234,15 @@ This document is published but is not visible because the parent '%0%' is unpublished This culture is published but is not visible because it is unpublished on parent '%0%' 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% - This document is published but its url cannot be routed + Could not get the URL + This document is published but its URL would collide with content %0% + This document is published but its URL cannot be routed Publish Published - Published (pending changes)> + Published (pending changes) Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + %0% and all content items underneath and thereby making their content publicly available.]]> + Publish at Unpublish at Clear Date @@ -264,6 +252,7 @@ Statistics Title (optional) Alternative text (optional) + Caption (optional) Type Unpublish Draft @@ -279,20 +268,20 @@ Child items Target This translates to the following time on the server: - What does this mean?]]> + What does this mean?]]> Are you sure you want to delete this item? Are you sure you want to delete all items? Property %0% uses editor %1% which is not supported by Nested Content. - No content types are configured for this property. - Add element type - Select element type - Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. + No Content Types are configured for this property. + Add Element Type + Select Element Type + Select the group whose properties should be displayed. If left blank, the first group on the Element Type will be used. Enter an angular expression to evaluate against each item for its name. Use to display the item index Add another text box Remove this text box Content root - Include drafts: also publish unpublished content items. + Include unpublished content items. This value is hidden. If you need access to view this value please contact your website administrator. This value is hidden. What languages would you like to publish? All languages with content are saved! @@ -306,8 +295,19 @@ Unpublished Languages Unmodified Languages These languages haven't been created - Ready to Publish? + + All new variants will be saved. + Which variants would you like to publish? + Choose which variants to be saved. + Pick variants to send for approval. + Set scheduled publishing... + Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. + The following variants is required for publishing to take place: + + We are not ready to Publish + Ready to publish? Ready to Save? + Reset focal point Send for approval Select the date and time to publish and/or unpublish the content item. Create new @@ -336,29 +336,53 @@ Failed to create a folder under parent id %0% Failed to rename the folder with id %0% Drag and drop your file(s) into the area + Upload is not allowed in this location. Create a new member All Members Member groups have no additional properties for editing. + + Failed to copy content type + Failed to move content type + + + Failed to copy media type + Failed to move media type + Auto pick + + + Failed to copy member type + 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 + Select the Document Type you want to make a content template for Enter a folder name Choose a type and a title - Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - Document Types within the Settings section.]]> + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> The selected page in the content tree doesn't allow for any pages to be created below it. - Edit permissions for this document type - Create a new document type - Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> - Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Edit permissions for this Document Type + Create a new Document Type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> + Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> The selected media in the tree doesn't allow for any other media to be created below it. - Edit permissions for this media type Document Type without a template + Edit permissions for this Media Type + Document Type without a template + Document Type with Template + The data definition for a content page that can be created by editors in the content tree and is directly accessible via a URL. + Document Type + The data definition for a content component that can be created by editors in the content tree and be picked on other pages but has no direct URL. + Element Type + Defines the schema for a repeating set of properties, for example, in a 'Block List' or 'Nested Content' property editor. + Composition + Defines a re-usable set of properties that can be included in the definition of multiple other Document Types. For example, a set of 'Common Page Settings'. + Folder + Used to organise the Document Types, Compositions and Element Types created in this Document Type tree. New folder - New data type + New Data Type New JavaScript file New empty partial view New partial view macro @@ -418,6 +442,9 @@ Close this window Are you sure you want to delete Are you sure you want to disable + Are you sure you want to remove + %0%]]> + %0%]]> Are you sure? Are you sure? Cut @@ -448,8 +475,8 @@ 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' + 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 @@ -481,6 +508,7 @@ Select member type Select node Select sections + Select user Select users No icons were found There are no parameters for this macro @@ -493,8 +521,12 @@ Un-link your account Select editor + Select configuration Select snippet This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. + %0%.]]> + %0% from the %1% group]]> + Yes, remove There are no dictionary items. @@ -533,7 +565,7 @@ Tools to manage the index fields The index cannot be read and will need to be rebuilt - The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation + The process is taking longer than expected, check the Umbraco log to see if there have been any errors during this operation This index cannot be rebuilt because it has no assigned IIndexPopulator @@ -556,11 +588,14 @@ #value or ?key=value Enter alias... Generating alias... + Create item + Edit + Name Create custom list view Remove custom list view - A content type, media type or member type with this alias already exists + A Content Type, Media Type or Member Type with this alias already exists Renamed @@ -580,9 +615,9 @@ 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 + 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 + and all property types & property data using this Data Type Select the folder to move to in the tree structure below was moved underneath @@ -638,6 +673,7 @@ Clear Close Close Window + Close Pane Comment Confirm Constrain @@ -646,6 +682,7 @@ Continue Copy Create + Crop section Database Date Default @@ -655,6 +692,7 @@ Design Dictionary Dimensions + Discard Down Download Edit @@ -667,6 +705,7 @@ First Focal point General + Generic Groups Group Height @@ -677,6 +716,7 @@ Id Import Include subfolders in search + Search only this folder Info Inner margin Insert @@ -738,6 +778,7 @@ Sort Status Submit + Success Type Type to search... under @@ -745,7 +786,7 @@ Update Upgrade Upload - Url + URL User Username Value @@ -768,13 +809,14 @@ Other Articles Videos - Clear Installing + Avatar for Blue + Add tab Add group Add property Add editor @@ -796,6 +838,7 @@ General Editor Toggle allow culture variants + Toggle allow segmentation Background color @@ -820,8 +863,8 @@

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.

]]> + done.
+ 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.]]> @@ -867,8 +910,8 @@ ]]> I want to start from scratch learn how) + Your website is completely empty at the moment, so that's perfect if you want to start from scratch and create your own Document Types and templates. + (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? @@ -881,7 +924,7 @@ 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, + "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. @@ -908,7 +951,7 @@ Get help from our award winning community, browse the documentation or watch som 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]]> +To manage your website, simply open the Umbraco backoffice and start adding content, updating the templates and stylesheets or add new functionality]]>
Connection to database failed. Umbraco Version 3 Umbraco Version 4 @@ -936,7 +979,7 @@ To manage your website, simply open the Umbraco back office and start adding con Log in below Sign in with Session timed out - © 2001 - %0%
Umbraco.com

]]>
+ © 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 @@ -987,14 +1030,14 @@ To manage your website, simply open the Umbraco back office and start adding con Password reset requested

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

@@ -1115,7 +1158,7 @@ To manage your website, simply open the Umbraco back office and start adding con
- + Click this link to reset your password
- EDIT
+ EDIT
@@ -1234,6 +1277,7 @@ To manage your website, simply open the Umbraco back office and start adding con All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... + Verified to work on Umbraco Cloud Paste with full formatting (Not recommended) @@ -1304,6 +1348,17 @@ To manage your website, simply open the Umbraco back office and start adding con 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 Trashed + Open in Media Library + Change Media Item + Reset media crop + Edit %0% on %1% + Discard creation? + + You have made changes to this content. Are you sure you want to discard them? + Remove? + Remove all medias? + Clipboard + Not allowed enter external link @@ -1315,13 +1370,16 @@ To manage your website, simply open the Umbraco back office and start adding con Enter the link - Reset crop + Reset crop Save crop Add new crop - Done - Undo edits + Done + Undo edits + User defined + Changes + Created Select a version to compare with the current version Current version Red text will not be shown in the selected version. , green means added]]> @@ -1352,7 +1410,7 @@ To manage your website, simply open the Umbraco back office and start adding con Default template - 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) + 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 @@ -1363,7 +1421,6 @@ To manage your website, simply open the Umbraco back office and start adding con 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. Create matching template Add icon @@ -1381,6 +1438,7 @@ To manage your website, simply open the Umbraco back office and start adding con Validation errors must be fixed before the item can be saved Failed Saved + Saved. To view the changes please reload your browser Insufficient user permissions, could not complete the operation Cancelled Operation was cancelled by a 3rd party add-in @@ -1458,8 +1516,8 @@ To manage your website, simply open the Umbraco back office and start adding con Invitation has been re-sent to %0% Cannot publish the document since the required '%0%' is not published Validation failed for language '%0%' - Document type was exported to file - An error occurred while exporting the document type + Document Type was exported to file + An error occurred while exporting the Document Type The release date cannot be in the past Cannot schedule the document for publishing since the required '%0%' is not published Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language @@ -1526,9 +1584,9 @@ To manage your website, simply open the Umbraco back office and start adding con ]]> Section Name Section is mandatory - + @section definition, otherwise an error is shown. - + ]]> Query builder items returned, in copy to clipboard @@ -1575,16 +1633,20 @@ To manage your website, simply open the Umbraco back office and start adding con This content is allowed here Click to embed Click to insert image + Click to insert macro 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 + Edit Grid Layout Adjust the layout by setting column widths and adding additional sections Row configurations Rows are predefined cells arranged horizontally Add row configuration + Edit row configuration Adjust the row by setting cell widths and adding additional cells + No further configuration available Columns Total combined number of columns in the grid layout Settings @@ -1598,7 +1660,7 @@ To manage your website, simply open the Umbraco back office and start adding con Set as default Choose extra Choose default - are added + are added Warning You are deleting the row configuration @@ -1608,6 +1670,7 @@ To manage your website, simply open the Umbraco back office and start adding con Compositions Group + You can't move the group %0% to this tab because the group will get the same alias as a tab: "%1%". Rename the group to continue. You have not added any groups Add group Inherited from @@ -1622,13 +1685,15 @@ To manage your website, simply open the Umbraco back office and start adding con 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. - Removing a composition will delete all the associated property data. Once you save the document type there's no way back. + 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. + Removing a composition will delete all the associated property data. Once you save the Document Type there's no way back. Create new Use existing Editor settings + Available configurations + Create a new configuration Configuration Yes, delete was moved underneath @@ -1636,12 +1701,12 @@ To manage your website, simply open the Umbraco back office and start adding con Select the folder to move Select the folder to copy to in the tree structure below - All Document types + 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 + 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 @@ -1653,16 +1718,33 @@ To manage your website, simply open the Umbraco back office and start adding con Allow this property value to be displayed on the member profile page tab has no sort order Where is this composition used? - This composition is currently used in the composition of the following content types: - Allow varying by culture + This composition is currently used in the composition of the following Content Types: + Allow variations + Allow vary by culture + Allow segmentation + Vary by culture + Vary by segments Allow editors to create content of this type in different languages. + Allow editors to create content of different languages. + Allow editors to create segments of this content. Allow varying by culture - Element type - Is an element type - An element type is meant to be used for instance in Nested Content, and not in the tree. - A document type cannot be changed to an element type once it has been used to create one or more content items. - This is not applicable for an element type + Allow segmentation + Element Type + Is an Element Type + An Element Type is meant to be used for instance in Nested Content, and not in the tree. + A Document Type cannot be changed to an Element Type once it has been used to create one or more content items. + This is not applicable for an Element Type You have made changes to this property. Are you sure you want to discard them? + Appearance + Label above (full-width) + %0%?]]> + %0%?]]> + %0%?]]> + This will also delete all items below this tab. + This will also delete all items below this group. + Add tab + Convert to tab + Drag properties here to place directly on the tab Add language @@ -1815,13 +1897,15 @@ To manage your website, simply open the Umbraco back office and start adding con Administrator Category field User created - Change Your Password + Change your password Change photo New password + Minimum %0% character(s) to go! + There should be at least %0% special character(s) in there. hasn't been locked out The password hasn't been changed 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 + You can change your password for accessing the Umbraco backoffice by filling out the form below and click the 'Change Password' button Content Channel Create another user Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. @@ -1852,6 +1936,7 @@ To manage your website, simply open the Umbraco back office and start adding con Password Reset password Your password has been changed! + Password changed Please confirm the new password Enter your new password Your new password cannot be blank! @@ -1953,7 +2038,7 @@ To manage your website, simply open the Umbraco back office and start adding con - + Click this link to accept the invite @@ -2018,10 +2103,14 @@ To manage your website, simply open the Umbraco back office and start adding con Enter a custom validation error message (optional) You need to add at least You can only have + Add up to items + URL(s) + URL(s) selected items selected Invalid date Not a number + Not a valid numeric step size Invalid email Value cannot be null Value cannot be empty @@ -2137,7 +2226,7 @@ To manage your website, simply open the Umbraco back office and start adding con The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. - You can add your own health checks, have a look at the documentation for more information about custom health checks.

+ You can add your own health checks, have a look at the documentation for more information about custom health checks.

]]> @@ -2147,7 +2236,7 @@ To manage your website, simply open the Umbraco back office and start adding con Culture Original URL Redirected To - Redirect Url Management + Redirect URL Management The following URLs redirect to this content item: No redirects have been made When a published page gets renamed or moved a redirect will automatically be made to the new page. @@ -2186,7 +2275,7 @@ To manage your website, simply open the Umbraco back office and start adding con Created Comment Name - No relations for this relation type. + No relations for this Relation Type Relation Type Relations @@ -2217,6 +2306,12 @@ To manage your website, simply open the Umbraco back office and start adding con Open backoffice search Open/Close backoffice help Open/Close your profile options + Setup Culture and Hostnames for %0% + Create new node under %0% + Setup access restrictions on %0% + Setup Permissions on %0% + Change sort order for %0% + Create Content Template based on %0% Open context menu for Current language Switch language to @@ -2224,7 +2319,7 @@ To manage your website, simply open the Umbraco back office and start adding con Partial View Partial View Macro Member - Data type + Data Type Search the redirect dashboard Search the user group section Search the users section @@ -2232,6 +2327,21 @@ To manage your website, simply open the Umbraco back office and start adding con Create Edit Name + Add new row + View more options + Search the Umbraco backoffice + Search for content nodes, media nodes etc. across the backoffice. + When autocomplete results are available, press up and down arrows, or use the tab key and use the enter key to select. + Path: + Found in + Has translation + Missing translation + Dictionary items + Select one of the options to edit the node. + Perform action %0% on the %1% node + Add image caption + Search content tree + Maximum amount References @@ -2248,8 +2358,14 @@ To manage your website, simply open the Umbraco back office and start adding con Used in Media + Delete Saved Search Log Levels + Select all + Deselect all Saved Searches + Save Search + Enter a friendly name for your search query + Filter Search Total Items Timestamp Level @@ -2274,14 +2390,28 @@ To manage your website, simply open the Umbraco back office and start adding con Find Logs with Namespace Find Logs with Machine Name Open + Polling + Every 2 seconds + Every 5 seconds + Every 10 seconds + Every 20 seconds + Every 30 seconds + Polling every 2s + Polling every 5s + Polling every 10s + Polling every 20s + Polling every 30s Copy %0% %0% from %1% + Collection of %0% Remove all items + Clear clipboard Open Property Actions + Close Property Actions Wait @@ -2359,7 +2489,7 @@ To manage your website, simply open the Umbraco back office and start adding con Hours of Umbraco training videos are only a click away Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

+

Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

]]>
To get you started @@ -2370,36 +2500,115 @@ To manage your website, simply open the Umbraco back office and start adding con Find out more in the Documentation section of Our Umbraco + Read more about working with the items in Settings in the Documentation section of Our Umbraco ]]> Community Forum + Ask a question in the Community Forum ]]> tutorial videos (some are free, some require a subscription) + Watch our tutorial videos (some are free, some require a subscription) ]]> productivity boosting tools and commercial support + Find out about our productivity boosting tools and commercial support ]]> training and certification opportunities + Find out about real-life training and certification opportunities ]]> Welcome to The Friendly CMS Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. - + Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + Pick Element Type + Attach a settings Element Type + Select view + Select stylesheet + Choose thumbnail + Create new Element Type + Custom stylesheet + Add stylesheet + Editor apperance + Data models + Catalogue appearance + Background color + Icon color + Content model + Label + Custom view + Show custom view description + Overwrite how this block appears in the backoffice UI. Pick a .html file containing your presentation. + Settings model + Overlay editor size + Add custom view + Add settings + Overwrite label template + %0%?]]> + %0%?]]> + The content of this block will still be present, editing of this content will no longer be available and will be shown as unsupported content. + + Thumbnail + Add thumbnail + Create empty + Clipboard + Settings + Advanced + Force hide content editor + You have made changes to this content. Are you sure you want to discard them? + Discard creation? + + Error! + The ElementType of this block does not exist anymore + Add content + Add %0% + Property '%0%' uses editor '%1%' which is not supported in blocks. + + + What are Content Templates? + Content Templates are pre-defined content that can be selected when creating a new content node. + How do I create a Content Template? + + There are two ways to create a Content Template:

+
    +
  • Right-click a content node and select "Create Content Template" to create a new Content Template.
  • +
  • Right-click the Content Templates tree in the Settings section and select the Document Type you want to create a Content Template for.
  • +
+

Once given a name, editors can start using the Content Template as a foundation for their new page.

+ ]]> +
+ How do I manage Content Templates? + You can edit and delete Content Templates from the "Content Templates" tree in the Settings section. Expand the Document Type which the Content Template is based on and click it to edit or delete it. + + + End + End preview mode + Preview website + Open website in preview mode + Preview website? + You have ended preview mode, do you want to enable it again to view the latest saved version of your website? + Preview latest version + View published version + View published version? + You are in Preview Mode, do you want exit in order to view the published version of your website? + View published version + Stay in preview mode + + + item returned + items returned + diff --git a/TestSite/Umbraco/Config/Lang/es.xml b/TestSite/Umbraco/Config/Lang/es.xml index 371e6de..2648852 100644 --- a/TestSite/Umbraco/Config/Lang/es.xml +++ b/TestSite/Umbraco/Config/Lang/es.xml @@ -124,7 +124,7 @@ Guardar y publicar Guardar y enviar para aprobación Guardar vista de lista - Previsualizar + Previsualizar La previsualización está deshabilitada porque no hay ninguna plantilla asignada Elegir estilo Mostrar estilos @@ -133,26 +133,6 @@ Deshacer Rehacer - - Para cambiar el tipo de documento al contenido seleccionado, primero selecciona uno de la lista de tipos válidos. - Entonces confirma el mapeo de propiedades del tipo actual al nuevo y haz clic en Guardar. - El contenido se ha vuelto a publicar. - Propiedad actual - Tipo actual - El tipo de contenido no se puede cambiar, porque no hay alternativas válidas para este contenido. - Tipo de documento cambiado - Mapeo de propiedades - Mapea a la propiedad - Nueva plantilla - Nuevo tipo - ninguno - Contenido - Selecciona un nuevo Tipo de Documento - El tipo de documento del contenido seleccionado ha sido cambiado correctamente a [new type] y las siguientes propiedades mapeadas: - a - No se ha podido completar el mapeo de propiedades porque uno o más propiedades tienen más de un mapeo definido. - Solo se muestran otros tipos válidos para el contenido actual. - Está publicado Acerca de @@ -185,8 +165,8 @@ Propiedades Este documento ha sido publicado pero no es visible porque el padre '%0%' no esta publicado Ups: este documento está publicado pero no está en la caché (error interno) - No se pudo obtener la url - Este documento está publicado pero tu url colisionará con contenido %0% + No se pudo obtener la URL + Este documento está publicado pero tu URL colisionará con contenido %0% Publicar Estado de la Publicación Publicar el @@ -208,7 +188,7 @@ Nodos hijo Destino Esto se traduce en la siguiente hora en el servidor: - ¿Esto qué significa?]]> + ¿Esto qué significa?]]> ¿Estás seguro que quieres eliminar este elemento? Propiedad %0% utiliza editor %1% que no está soportado por Nested Content. Añadir otra caja de texto @@ -320,8 +300,8 @@ Se está vaciando la papelera. No cierres esta ventana mientras se ejecuta este proceso La papelera está vacía No podrás recuperar los elementos una vez sean borrados de la papelera - regexlib.com está experimentando algunos problemas en estos momentos, de los cuales no somos responsables. Pedimos disculpas por las molestias.]]> - Buscar una expresión regular para agregar validación a un campo de formulario. Ejemplo: 'correo electrónico', código postal "," url " + regexlib.com está experimentando algunos problemas en estos momentos, de los cuales no somos responsables. Pedimos disculpas por las molestias.]]> + Buscar una expresión regular para agregar validación a un campo de formulario. Ejemplo: 'correo electrónico', 'código postal', 'URL'. Eliminar macro Campo obligatorio El sitio ha sido reindexado @@ -504,11 +484,11 @@ Margen interno Insertar Instalar - Inválido + Inválido Justificar Etiqueta Idioma - Último + Último Diseño Cargando Bloqueado @@ -520,7 +500,7 @@ Mensaje Mover Nombre - Nuevo + Nuevo Próximo No de @@ -562,7 +542,7 @@ Actualizar Actualizar Subir - Url + URL Usuario Nombre de usuario Valor @@ -625,7 +605,7 @@ Configuración de la base de datos instalar para instalar %0% la base de datos de Umbraco]]> Próximo para continuar]]> - ¡No se ha encontrado ninguna base de datos! Mira si la información en la cadena de conexión del “web.config” es correcta.

Para continuar, edita el "web.config" (bien sea usando Visual Studio o tu editor de texto preferido), ve al final del archivo y añade la cadena de conexión para la base de datos con el nombre (key) "umbracoDbDSN" y guarda el archivo.

Pincha en reintentar cuando hayas terminado.
Pincha aquí para mayor información de como editar el web.config (en inglés)

]]>
+ ¡No se ha encontrado ninguna base de datos! Mira si la información en la cadena de conexión del “web.config” es correcta.

Para continuar, edita el "web.config" (bien sea usando Visual Studio o tu editor de texto preferido), ve al final del archivo y añade la cadena de conexión para la base de datos con el nombre (key) "umbracoDbDSN" y guarda el archivo.

Pincha en reintentar cuando hayas terminado.
Pincha aquí para mayor información de como editar el web.config (en inglés)

]]>
Por favor, contacta con tu ISP si es necesario. Si estás realizando la instalación en una máquina o servidor local, quizás necesites información de tu administrador de sistemas.]]> Pincha en actualizar para actualizar la base de datos a Umbraco %0%

Ningún contenido será borrado de la base de datos y seguirá funcionando después de la actualización

]]>
Pincha en Próximo para continuar. ]]> @@ -652,7 +632,7 @@ Configurando los permisos de directorios Umbraco necesita permisos de lectura/escritura en algunos directorios para poder almacenar archivos tales como imágenes y PDFs. También almacena datos en la caché para mejorar el rendimiento de tu sitio web Quiero empezar de cero - aprende cómo). Todavía podrás elegir instalar Runway más adelante. Por favor ve a la sección del Desarrollador y elige Paquetes.]]> + aprende cómo). Todavía podrás elegir instalar Runway más adelante. Por favor ve a la sección del Desarrollador y elige Paquetes.]]> Acabas de configurar una nueva plataforma Umbraco. ¿Qué deseas hacer ahora? Se ha instalado Runway Esta es nuestra lista de módulos recomendados, selecciona los que desees instalar, o mira la lista completa de módulos ]]> @@ -671,7 +651,7 @@ Umbraco %0% ha sido instalado y está listo para ser usado archivo /web.config y actualizar la clave del AppSetting UmbracoConfigurationStatus del final al valor '%0%'.]]> empezar inmediatamente pulsando el botón "Lanzar Umbraco" de debajo.
Si eres nuevo con Umbraco, puedes encontrar cantidad de recursos en nuestras páginas de cómo empezar.]]>
- Lanzar Umbraco Para administrar tu sitio web, simplemente abre el back office de Umbraco y empieza a añadir contenido, a actualizar plantillas y hojas de estilo o a añadir nueva funcionalidad]]> + Lanzar Umbraco Para administrar tu sitio web, simplemente abre el backoffice de Umbraco y empieza a añadir contenido, a actualizar plantillas y hojas de estilo o a añadir nueva funcionalidad]]> No se ha podido establecer la conexión con la base de datos Umbraco versión 3 Umbraco versión 4 @@ -696,7 +676,7 @@ Resplandeciente sábado Iniciar sesión La sesión ha caducado - © 2001 - %0%
umbraco.com

]]>
+ © 2001 - %0%
umbraco.com

]]>
¿Olvidaste tu contraseña? Enviaremos un email a la dirección especificada con un enlace para restaurar tu contraseña Un email con instrucciones para restaurar tu contraseña será enviado a la dirección especificada si ésta está registrada. @@ -752,7 +732,7 @@ - + Pulsa este enlace para restaurar tu contraseña @@ -989,7 +969,6 @@ Pestañas Tipo de Contenido Maestro activado Este Tipo de Contenido usa - como Tipo de Contenido Maestro. Las pestañas para los Tipos de Contenido Maestros no se muestran y solo se pueden modificar desde el Tipo de Contenido Maestro No existen propiedades para esta pestaña. Haz clic en el enlace "añadir nueva propiedad" para crear una nueva propiedad. Añadir icono @@ -1114,9 +1093,9 @@ ]]> Nombre de sección Sección es obligatoria - + @section o se mostrará un error. - + ]]> Constructor de consultas elementos devueltos, en Quiero @@ -1284,7 +1263,7 @@ Este mail se ha generado automáticamente para informale que %2% has solicitado que el documento '%1%' sea traducido en '%5%'. - Para editarlo, vaya a la dirección http://%3%/translation/details.aspx?id=%4% o inicia sesión en umbraco y ve a http://%3% para ver las tareas pendientes de traducir. + Para editarlo, vaya a la dirección http://%3%/translation/details.aspx?id=%4% o inicia sesión en Umbraco y ve a http://%3% para ver las tareas pendientes de traducir. Espero que tenga un buen dia. @@ -1485,7 +1464,7 @@ - + Pulsa este enlace para aceptar la invitación @@ -1526,7 +1505,7 @@ Validación Validar como email Validar como número - Validar como Url + Validar como URL ...o introduce tu propia validación Campo obligatorio Introduce una expresión regular @@ -1648,4 +1627,8 @@ caracteres restantes + + Seleccionar todo + Deseleccionar todo + diff --git a/TestSite/Umbraco/Config/Lang/fr.xml b/TestSite/Umbraco/Config/Lang/fr.xml index 18a5fe9..538f334 100644 --- a/TestSite/Umbraco/Config/Lang/fr.xml +++ b/TestSite/Umbraco/Config/Lang/fr.xml @@ -12,8 +12,8 @@ Copier Créer Exporter - Créer un groupe Créer un package + Créer un groupe Supprimer Désactiver Vider la corbeille @@ -30,17 +30,21 @@ Dépublier Rafraîchir Republier le site tout entier + Renommer Récupérer Spécifiez les permissions pour la page %0% + Choisissez où copier Choisissez où déplacer dans l'arborescence ci-dessous + a été déplacé vers + a été copié vers + a été supprimé Permissions Version antérieure Envoyer pour publication Envoyer pour traduction Spécifier le groupe Trier - Envoyer pour publication Traduire Mettre à jour Spécifier les permissions @@ -65,6 +69,7 @@ Permettre de déplacer un noeud Permettre de définir et modifier l'accès public à un noeud Permettre de publier un noeud + Permettre d'annuler la publication d'un noeud Permettre de modifier les permissions pour un noeud Permettre de revenir à une situation antérieure Permettre d'envoyer un noeud pour approbation avant publication @@ -74,6 +79,10 @@ Permettre de sauvegarder un noeud Permettre la création d'un Modèle de Contenu + + Contenu + Info + Permission refusée. Ajouter un nouveau domaine @@ -88,15 +97,15 @@ Domaine '%0%' déjà assigné Domaine '%0%' mis à jour Editer les domaines actuels + + Hériter Culture ou hériter de la culture des noeuds parents. S'appliquera aussi
au noeud courant, à moins qu'un domaine ci-dessous soit aussi d'application.]]>
Domaines - - Aperçu pour - Vider la sélection Choisir @@ -117,41 +126,68 @@ Liste numérique Insérer une macro Insérer une image + Publier et fermer + Publier avec les descendants Editer les relations Retourner à la liste Sauver + Sauver et fermer Sauver et publier Sauver et planifier Sauver et envoyer pour approbation Sauver la mise en page de la liste - Prévisualiser + Planifier + Prévisualiser + Prévisualiser La prévisualisation est désactivée car aucun modèle n'a été assigné. Choisir un style Afficher les styles Insérer un tableau + Générer les modèles et fermer Sauver et générer les modèles Défaire Refaire + Restaurer + Supprimer un tag + Annuler + Confirmer + Options de publication supplémentaires - - Pour changer le type de document du contenu séléctionné, faites d'abord un choix dans la liste des types valides à cet endroit. - Puis modifiez si nécessaire la correspondance des propriétés du type actuel vers le nouveau, et cliquez sur Sauver. - Le contenu a été republié. - Propriété actuelle - Type actuel - Le type de document ne peut être changé car il n'y a pas d'alternative valide à cet endroit. Une alternative sera valide si elle est autorisée sous le parent du contenu sélectionné et si tous les éléments de contenu enfants existants peuvent être créés avec celle-ci. - Type de document modifié - Faire correspondre les propriétés - Faire correspondre à la propriété - Nouveau modèle - Nouveau type - aucun - Contenu - Choisir le nouveau Type de Document - Le type de document du contenu séléctionné a bien été changé en [new type] et les correspondances de propriétés suivantes effectuées : - en - Impossible de terminer la correspondance des propriétés car une ou plusieurs propriétés ont plus d'une correspondance définie. - Seuls les types de documents valides à cet endroit sont affichés. + + Aperçu pour + Contenu supprimé + Contenu dé-publié + Contenu dé-publié pour les langues : %0% + Contenu publié + Contenu publié pour les langues : %0% + Contenu sauvegardé + Contenu sauvegardé pour les langues : %0% + Contenu déplacé + Contenu copié + Contenu restauré + Contenu envoyé pour publication + Contenu envoyé pour publication pour les langues : %0% + Ordonnancement des sous-éléments réalisé par l'utilisateur + Copier + Publier + Publier + Déplacer + Sauvegarder + Sauvegarder + Supprimer + Annuler publication + Annuler publication + Restaurer + Envoyer pour publication + Envoyer pour publication + Ordonner + Historique (toutes variantes) + + + Echec de la création d'un dossier sous le parent avec l'ID %0% + Echec de la création d'un dossier sous le parent avec le nom %0% + Le nom du dossier ne peut pas contenir de caractères illégaux. + Echec de la suppression de l'élément : %0% A été publié @@ -184,15 +220,20 @@ Aucune date choisie Titre de la page Ce média n'a pas de lien + Aucun contenu ne peut être ajouté pour cet élément Propriétés Ce document est publié mais n'est pas visible car son parent '%0%' n'est pas publié - Oups : ce document est publié mais n'est pas présent dans le cache (erreur interne Umbraco) - Oups: impossible d'obtenir cet url (erreur interne - voir fichier log) - Oups: ce document est publié mais son url entrerait en collision avec le contenu %0% + Cette culture est publiée mais n'est pas visible car elle n'est pas publiée pour le parent '%0%' + Ce document est publié mais n'est pas présent dans le cache + Oups: impossible d'obtenir cet URL (erreur interne - voir fichier log) + Ce document est publié mais son URL entrerait en collision avec le contenu %0% + Ce document est publié mais son URL ne peut pas être routé Publier Publié Publié (changements en cours) Statut de publication + Publier avec ses descendants pour publier %0% et tous les éléments de contenu en-dessous, rendant de ce fait leur contenu accessible publiquement.]]> + Publier avec ses descendants pour publier les langues sélectionnées et les mêmes langues des éléments de contenu en-dessous, rendant de ce fait leur contenu accessible publiquement.]]> Publié le Dépublié le Supprimer la date @@ -202,9 +243,11 @@ Statistiques Titre (optionnel) Texte alternatif (optionnel) + Légende (optionnel) Type Dépublier Dépublié + Non créé Dernière édition Date/heure à laquelle ce document a été édité Supprimer le(s) fichier(s) @@ -214,14 +257,36 @@ Eléments enfants Cible Ceci se traduit par l'heure suivante sur le serveur : - Qu'est-ce que cela signifie?]]> - Etes-vous certain(e) de vouloir supprimer cet éléménent? + Qu'est-ce que cela signifie?]]> + Etes-vous certain(e) de vouloir supprimer cet élément? + Etes-vous certain(e) de vouloir supprimer tous les éléments? La propriété %0% utilise l'éditeur %1% qui n'est pas supporté par Nested Content. + Aucun type de contenu n'est configuré pour cette propriété. + Ajouter un type d'élément + Sélectionner un type d'élément Ajouter un autre champ texte Enlever ce champ texte Racine du contenu + Inclure les brouillons : publier également les éléments de contenu non publiés. Cette valeur est masquée. Si vous avez besoin de pouvoir accéder à cette valeur, veuillez prendre contact avec l'administrateur du site web. Cette valeur est masquée. + Quelles langues souhaitez-vous publier? Toutes les langues ayant du contenu ont été sauvegardées! + Quelles langues souhaitez-vous publier? + Quelles langues souhaitez-vous sauvegarder? + Toutes les langues avec du contenu sont sauvegardées lors de la création! + Quells langues souhaitez-vous envoyer pour approbation? + Quelles langues souhaitez-vous planifier? + Sélectionnez les langues à dépublier. La dépublication d'une langue obligatoire provoquera la dépublication de toutes les langues. + Langues publiées + Langues non publiées + Langues non modifiées + Ces langues n'ont pas été créées + Prêt.e à publier? + Prêt.e à sauvegarder? + Envoyer pour approbation + Sélectionnez la date et l'heure de publication/dépublication de l'élément de contenu. + Créer nouveau + Copier du clipboard Créer un nouveau Modèle de Contenu à partir de '%0%' @@ -235,22 +300,49 @@ Cliquez pour télécharger ou cliquez ici pour choisir un fichier + Vous pouvez faire glisser des fichiers ici pour télécharger. Ce fichier ne peut pas ête chargé, il n'est pas d'un type de fichier autorisé. La taille maximum de fichier est Racine du média + Echec du déplacement du média + Les dossiers parent et destination ne peuvent pas être identiques + Echec de la copie du media + Echec de la création d'un dossier sous le parent avec l'id %0% + Echec du changement de nom du dossier avec l'id %0% + Glissez et déposez vos fichiers dans la zone Créer un nouveau membre Tous les membres + Les groupes de membres n'ont pas de propriétés supplémentaires modifiables. + + + Echec de la copie du type de contenu + Echec du déplacement du type de contenu + + + Echec de la copie du type de media + Echec du déplacement du type de media + + + Echec de la copie du type de membre Où voulez-vous créer le nouveau %0% Créer un élément sous Sélectionnez le type de document pour lequel vous souhaitez créer un modèle de contenu + Introduisez un nom de dossier Choisissez un type et un titre - "Types de documents".]]> - "Types de médias".]]> - Type de document sans modèle + Types de documents sous la section Paramètres, en modifiant les Types de noeuds enfants autorisés sous les Permissions.]]> + Types de documents sous la section Paramètres.]]> + La page sélectionnée dans l'arborescence de contenu n'autorise pas la création de pages sous elle. + Modifier les permissions pour ce type de document + Créer un nouveau type de document + Types de documents sous la section Paramètres, en modifiant l'option Autoriser comme racine sous les Permissions.]]> + Types de médias dans la section Paramètres, en modifiant les Types de noeuds enfants autorisés sous les Permissions.]]> + Le media sélectionné dans l'arborescence n'autorise pas la création d'un autre media sous lui. + Modifier les permissions pour ce type de media + Type de document sans modèle Nouveau répertoire Nouveau type de données Nouveau fichier javascript @@ -259,6 +351,8 @@ Nouvelle vue partielle à partir d'un snippet Nouvelle macro pour vue partielle à partir d'un snippet Nouvelle macro pour vue partielle (sans macro) + Nouveau fichier de feuille de style + Nouveau fichier de feuille de style pour l'éditeur de texte Parcourir votre site @@ -274,7 +368,10 @@ Invalider les changements Vous avez des changements en cours Etes-vous certain(e) de vouloir quitter cette page? - vous avez des changements en cours - La dépublication va supprimer du site cette page ainsi que tous ses descendants. + La publication rendra les éléments sélectionnés visibles sur le site. + La suppression de la publication supprimera du site les éléments sélectionnés et tous leurs descendants. + La suppression de la publication supprimera du site cette page ainsi que tous ses descendants. + Vous avez des modifications en cours. Modifier le Type de Document fera disparaître ces modifications. Terminé @@ -302,8 +399,8 @@ Titre du lien Lien + Ancrage / requête Nom - Gérer les noms d'hôtes Fermer cette fenêtre Êtes-vous certain(e) de vouloir supprimer Êtes-vous certain(e) de vouloir désactiver @@ -312,6 +409,7 @@ Couper Editer une entrée du Dictionnaire Modifier la langue + Modifier le media sélectionné Insérer un lien local (ancre) Insérer un caractère Insérer un entête graphique @@ -319,22 +417,25 @@ Insérer un lien Insérer une macro Insérer un tableau + Ceci supprimera la langue + Modifier la culture d'une langue peut être une opération lourde qui aura pour conséquence la réinitialisation de la cache de contenu et des index Dernière modification Lien Lien interne : Si vous utilisez des ancres, insérez # au début du lien Ouvrir dans une nouvelle fenêtre? + Paramètres de la macro Cette macro ne contient aucune propriété éditable Coller Editer les permissions pour Définir les permissions pour Définir les permissions pour %0% pour le groupe d'utilisateurs %1% Sélectionnez les groupes d'utilisateurs pour lesquels vous souhaitez définir les permissions - Les éléments dans la corbeille sont en cours de suppression. Ne fermez pas cette fenêtre avant que cette opération soit terminée. + Les éléments dans la corbeille sont en cours de suppression. Veuillez ne pas fermer cette fenêtre avant que cette opération ne soit terminée. La corbeille est maintenant vide Les éléments supprimés de la corbeille seront supprimés définitivement - regexlib.com rencontre actuellement des problèmes sur lesquels nous n'avons aucun contrôle. Nous sommes sincèrement désolés pour le désagrément.]]> - Rechercher une expression régulière à ajouter pour la validation d'un champ de formulaire. Exemple: 'email, 'zip-code', 'url' + regexlib.com rencontre actuellement des problèmes sur lesquels nous n'avons aucun contrôle. Nous sommes sincèrement désolés pour le désagrément.]]> + Rechercher une expression régulière à ajouter pour la validation d'un champ de formulaire. Exemple: 'email, 'zip-code', 'URL'. Supprimer la macro Champ obligatoire Le site a été réindéxé @@ -353,14 +454,17 @@ Lier à un media Sélectionner le noeud de base du contenu Sélectionner le media + Sélectionner le type de media Sélectionner l'icône Sélectionner l'élément Sélectionner le lien Sélectionner la macro Sélectionner le contenu + Sélectionner le type de contenu Sélectionner le noeud de base des media Sélectionner le membre Sélectionner le groupe de membres + Sélectionner le type de membre Sélectionner le noeud Sélectionner les sections Sélectionner les utilisateurs @@ -376,10 +480,14 @@ compte Sélectionner un éditeur Selectionner un snippet + Ceci supprimera le noeud et toutes ses langues. Si vous souhaitez supprimer une langue spécifique, vous devriez plutôt supprimer la publication du noeud dans cette langue-là. + + + Il n'y a pas d'éléments dans le dictionnaire. %0%' ci-dessous. + Editez les différentes versions de langues pour l'élément de dictionnaire '%0%' ci-dessous. ]]> Nom de Culture Aperçu du dictionaire + + Recherches configurées + Affiche les propriétés et les outils de chaque Recherche configurée (e.g. une recherche multi-index) + Valeurs du champ + Etat de santé + L'état de santé de l'index et s'il peut être lu + Indexeurs + Info Index + Liste les propriétés de l'index + Gérer les index d'Examine + Vous permet de voir les détails de chaque index et fournit des outils pour gérer les index + Reconstruire l'index + + Cela pourrait prendre un certain temps en fonction de la quantité de contenu présente dans votre site.
+ Il est déconseillé de reconstruire un index pendant les périodes de trafic intense sur le site web ou quand les éditeurs sont en train d'éditer du contenu. + ]]> +
+ Recherches + Rechercher dans l'index et afficher les résultats + Outils + Outils pour gérer l'index + champs + L'index ne peut pas être lu et devra être reconstruit + Le processus dure plus de temps que prévu, vérifiez les logs Umbraco afin de voir s'il y a eu des erreurs pendant cette opératon + Cet index ne peut pas être reconstruit parce qu'on ne lui a pas assigné de + IIndexPopulator + Votre nom d'utilisateur Votre mot de passe @@ -403,10 +539,18 @@ Entrez votre email Entrez un message... Votre nom d'utilisateur est généralement votre adresse email + #value ou ?key=value + Introduisez l'alias... + Génération de l'alias... + Créer un élément + Créer + Modifier + Nom Créer une liste personnalisée Supprimer la liste personnalisée + Il existe déjà un type de contenu, un tye de media ou un type de membre avec cet alias Renommé @@ -432,9 +576,11 @@ Sélectionnez le répertoire où déplacer dans l'arborescence ci-dessous a été déplacé sous + %0% supprimera les propriétés et leurs données des éléments suivants]]> + Je comprends que cette action va supprimer les propriétés et les données basées sur ce Type de Données - Vos données ont été sauvegardées, mais avant de pouvoir publier votre page, il y a des erreurs que vous devez corriger : + Vos données ont été sauvegardées, mais avant de pouvoir publier votre page, il y a des erreurs que vous devez d'abord corriger : Le Membership Provider n'autorise pas le changement des mots de passe (EnablePasswordRetrieval doit être défini à true) %0% existe déjà Des erreurs sont survenues : @@ -459,8 +605,9 @@ Noeud de départ supprimé, contactez votre administrateur Veuillez sélectionner du contenu avant de changer le style Aucun style actif disponible - Veuillez placer le curseur à gauche des deux cellules que vous voulez fusionner + Veuillez placer le curseur à la gauche des deux cellules que vous voulez fusionner Vous ne pouvez pas scinder une cellule qui n'a pas été fusionnée. + Cette propriété n'est pas valide Options @@ -472,6 +619,7 @@ Tout Êtes-vous certain(e)? Retour + Retour à l'aperçu Bord par Annuler @@ -479,10 +627,12 @@ Choisir Fermer Fermer la fenêtre + Fermer le panel Commenter Confirmer Conserver Conserver les proportions + Contenu Continuer Copier Créer @@ -502,16 +652,21 @@ Eléments Email Erreur + Champ Trouver Premier + Point focal Général Groupes + Groupe Hauteur Aide Cacher Historique Icône + Id Importer + Inclure les sous-dossiers dans la recherche Info Marge intérieure Insérer @@ -548,9 +703,11 @@ Un moment s'il vous plaît... Précédent Propriétés + Reconstruire Email de réception des données de formulaire Corbeille Votre corbeille est vide + Rafraîchir Restant Enlever Renommer @@ -564,6 +721,7 @@ Désolé, nous ne pouvons pas trouver ce que vous recherchez Aucun élément n'a été ajouté Serveur + Paramètres Montrer Afficher la page à l'envoi Taille @@ -572,11 +730,12 @@ Envoyer Type Rechercher... + sous Haut Mettre à jour Upgrader Télécharger - Url + URL Utilisateur Nom d'utilisateur Valeur @@ -596,6 +755,7 @@ actuel Intégrer sélectionné + Avatar de Bleu @@ -611,8 +771,8 @@ Parcourir les sections Raccourcis afficher les raccourcis - Passer à la vue en liste - Basculer vers l'autorisation comme racine + Activer / Désactiver la vue en liste + Activer / Désactiver l'autorisation comme racine Commenter/Décommenter les lignes Supprimer la ligne Copier les lignes vers le haut @@ -621,6 +781,7 @@ Déplacer les lignes vers le bas Général Editeur + Activer / Désactiver les variantes de culture Couleur de fond @@ -645,7 +806,7 @@

Pour poursuivre, veuillez éditer le fichier "web.config" (avec Visual Studio ou votre éditeur de texte favori), scroller jusqu'en bas, ajouter le "connection string" pour votre base de données dans la ligne avec la clé "umbracoDbDSN" et sauvegarder le fichier.

Cliquez sur le bouton Réessayer lorsque cela est fait. -
+
Plus d'informations sur l'édition du fichier web.config ici.

]]> Veuillez contacter votre fournisseur de services internet si nécessaire. @@ -693,7 +854,7 @@ Je veux démarrer "from scratch" Apprenez comment) + (Apprenez comment) Vous pouvez toujours choisir d'installer Runway plus tard. Pour cela, allez dans la section "Développeur" et sélectionnez "Packages". ]]> Vous venez de mettre en place une plateforme Umbraco toute nette. Que voulez-vous faire ensuite ? @@ -761,7 +922,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Connectez-vous ci-dessous Identifiez-vous avec La session a expiré - © 2001 - %0%
Umbraco.com

]]>
+ © 2001 - %0%
Umbraco.com

]]>
Mot de passe oublié? Un email contenant un lien pour ré-initialiser votre mot de passe sera envoyé à l'adresse spécifiée Un email contenant les instructions de ré-initialisation de votre mot de passe sera envoyée à l'adresse spécifiée si elle correspond à nos informations. @@ -812,14 +973,14 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Une réinitialisation de votre mot de passe a été demandée

- Votre nom d'utilisateur pour vous connecter au back-office Umbraco est : %0% + Votre nom d'utilisateur pour vous connecter au backoffice Umbraco est : %0%

@@ -875,6 +1036,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Editez vos notifications pour %0% + Paramètres de notification enregistrés pour + Les langues suivantes ont été modifiées : %0% @@ -936,7 +1099,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à
- + Cliquez sur ce lien pour réinitialiser votre mot de passe
- MODIFIER
+ MODIFIER
@@ -966,29 +1129,50 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à ]]> + Les langues suivantes ont été modifiées :

+ %0% + ]]>
La notification [%0%] à propos de %1% a été executée sur %2% Notifications + Actions + Créé + Créer un package et localisez le package. Les packages Umbraco ont généralement une extension ".umb" ou ".zip". ]]> + Ceci va supprimer le package Déposez pour uploader + Inclure tous les noeuds enfant 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 + Chemin du fichier + Le chemin absolu du fichier (eg: /bin/umbraco.bin) + Installé Packages installés + Installer localement + Terminer + Ce package n'a pas de vue de configuration + Aucun package n'a encore été créé Vous n'avez aucun package installé 'Packages' en haut à droite de votre écran]]> - Chercher des packages + Actions du package + URL de l'auteur + Contenu du package + Fichiers du package + URL de l'icone + Installer le package + Licence + URL de la licence + Propriétés du package + 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 @@ -1025,6 +1209,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Remarque : tous les documents, media etc. dépendant des éléments que vous supprimez vont cesser de fonctionner, ce qui peut provoquer une instabilité du système, désinstallez donc avec prudence. En cas de doute, contactez l'auteur du package.]]> Version du package + Mise à jour à partir de la version Package déjà installé Ce package ne peut pas être installé, il nécessite au minimum la version Umbraco %0% Désinstallation... @@ -1043,24 +1228,29 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Coller, mais supprimer le formatage (recommandé) - Protection basée sur les rôles - via les groupes de membres Umbraco.]]> - Vous devez créer un groupe avant de pouvoir utiliser l'authentification basée sur les rôles + Protection basée sur les groupes + Si vous souhaitez donner accès à tous les utilisateurs de groupes de membres spécifiques + Vous devez créer un groupe de membres avant de pouvoir utiliser la protection basée sur les groupes Page d'erreur Utilisé pour les personnes connectées, mais qui n'ont pas accès - Choisissez comment restreindre l'accès à cette page - %0% est maintenant protégée - Protection supprimée de %0% + %0%]]> + %0% est maintenant protégée]]> + %0% supprimée]]> Page de connexion Choisissez la page qui contient le formulaire de connexion - Supprimer la protection + Supprimer la protection... + %0%?]]> Choisissez les pages qui contiennent le formulaire de connexion et les messages d'erreur - Choisissez les rôles qui ont accès à cette page - Définissez l'identifiant et le mot de passe pour cette page - Protection utilisateur unique - Si vous souhaitez mettre en place une protection simple utilisant un identifiant et un mot de passe uniques + %0%]]> + %0%]]> + Protection pour des membres spécifiques + Si vous souhaitez donner accès à des membres spécifiques + Permissions utilisateur insuffisantes pour publier tous les documents enfants. + @@ -1076,7 +1266,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à - Inclure les pages enfant non publiées + + La validation a échoué pour la langue obligatoire '%0%'. Cette langue a été sauvegardée mais pas publiée. Publication en cours - veuillez patienter... %0% pages sur %1% ont été publiées... %0% a été publié @@ -1090,12 +1281,15 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Vous n'avez configuré aucune couleur approuvée + Vous pouvez uniquement sélectionner des éléments du(des) type(s) : %0% Vous avez choisi un élément de contenu actuellement supprimé ou dans la corbeille Vous avez choisi des éléments de contenu actuellement supprimés ou dans la corbeille + Elément supprimé Vous avez choisi un élément media actuellement supprimé ou dans la corbeille Vous avez choisi des éléments media actuellement supprimés ou dans la corbeille + Mis dans la corbeille introduire un lien externe @@ -1108,11 +1302,13 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Réinitialiser - Définir le recadrage Sauvegarder le recadrage Ajouter un nouveau recadrage + Terminé + Annuler les modifications + Sélectionnez une version à comparer avec la version actuelle Version actuelle Le texte en Rouge signifie qu'il a été supprimé de la version choisie, vert signifie ajouté]]> Le document a été restauré à une version antérieure @@ -1125,20 +1321,14 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Editer le fichier de script - Concierge Contenu - Courier - Développeur - Assistant de configuration Umbraco + Formulaires Medias Membres - Newsletters + Packages Configuration - Statistiques Traduction Utilisateurs - Aide - Formulaires Les meilleurs tutoriels vidéo Umbraco @@ -1156,31 +1346,32 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Onglets Type de contenu de base activé Ce type de contenu utilise - en tant que type de contenu de base. Les onglets du type de contenu de base ne sont pas affichés et peuvent seulement être modifiés à partir du type de contenu de base lui-même. Aucune propriété définie dans cet onglet. Cliquez sur le lien "Ajouter une nouvelle propriété" en-haut pour créer une nouvelle propriété. + Créer le template correspondant Ajouter une icône Ordre de tri 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 + 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 en-têtes de colonnes pour trier la collection complète d'éléments + Ce noeud n'a aucun noeud enfant à trier Validation Les erreurs de validation doivent être corrigées avant de pouvoir sauvegarder l'élément Echec Sauvegardé + Sauvegardé. Veuillez rafraîchir votre navigateur pour voir les changements Permissions utilisateur insuffisantes, l'opération n'a pas pu être complétée Annulation L'opération a été annulée par une extension tierce - La publication a été annulée par une extension tierce. Le type de propriété existe déjà Type de propriété créé Type de données : %1%]]> Type de propriété supprimé - Type de documet sauvegardé + Type de document sauvegardé Onglet créé Onglet supprimé Onglet avec l'ID : %0% supprimé @@ -1189,16 +1380,22 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Feuille de style sauvegardée sans erreurs Type de données sauvegardé Elément de dictionnaire sauvegardé - La publication a échoué car la page parent n'est pas publiée Contenu publié et visible sur le site + %0% documents publiés et visibles sur le site web + %0% publié et visible sur le site web + %0% documents publiés pour la langue %1% et visibles sur le site web Contenu sauvegardé N'oubliez pas de publier pour rendre les modifications visibles + Un planning de publication a été mis à jour + %0% sauvegardé Envoyer pour approbation Les modifications ont été envoyées pour approbation + %0% modifications ont été envoyées pour approbation Media sauvegardé Media sauvegardé sans erreurs Membre sauvegardé + Groupe de membres sauvegardé Propriété de feuille de style sauvegardée Feuille de style sauvegardée Modèle sauvegardé @@ -1213,11 +1410,14 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Langue sauvegardée Type de média sauvegardé Type de membre sauvegardé + Groupe de membres sauvegardé Modèle non sauvegardé Assurez-vous de ne pas avoir 2 modèles avec le même alias. Modèle sauvegardé Modèle sauvegardé sans aucune erreurs ! Contenu publié + Variation de contenu %0% dépubliée + La langue obligatoire '%0%' a été dépubliée. Toutes les langues pour cet éléménent de contenu sont maintenant dépubliées. Vue partielle sauvegardée Vue partielle sauvegardée sans erreurs ! Vue partielle non sauvegardée @@ -1237,16 +1437,35 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à L'utilisateur %0% a été supprimé Inviter l'utilisateur L'invitation a été envoyée à nouveau à %0% + Impossible de publier le document car la langue obligatoire '%0%' n'est pas publiée + La validation a échoué pour la langue '%0%' + Le Type de Document a été exporté dans le fichier + Une erreur est survenue durant l'export du type de document + La date de publication ne peut pas être dans le passé + Impossible de planifier la publication du document car la langue obligatoire '%0%' n'est pas publiée + Impossible de planifier la publication du document car la langue obligatoire '%0%' a une date de publication postérieure à celle d'une langue non obligatoire + La date d'expiration ne peut pas être dans le passé + La date d'expiration ne peut pas être antérieure à la date de publication - Utilise la syntaxe CSS. Ex : h1, .redHeader, .blueTex + Ajouter un style + Modifier un style + Styles pour l'éditeur de texte + Definir les styles qui doivent êtres disponibles dans l'éditeur de texte pour cette feuille de style Editer la feuille de style Editer la propriété de feuille de style Donner un nom pour identifier la propriété dans le Rich Text Editor Prévisualiser + L'apparence qu'aura le text dans l'éditeur de texte. + Sélecteur + Utilise la syntaxe CSS. Ex : "h1" ou ".redHeader" Styles + Le CSS qui devrait être appliqué dans l'éditeur de texte, e.g. "color:red;" + Code + Editeur de Texte + Echec de la suppression du modèle avec l'ID %0% Editer le modèle Sections Insérer une zone de contenu @@ -1288,11 +1507,12 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à ]]> Nom de la section La section est obligatoire - + @section, sinon une erreur est affichée. - + ]]> Générateur de requêtes éléments trouvés, en + copier dans le clipboard Je veux tout le contenu le contenu du type "%0%" @@ -1336,6 +1556,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Ce contenu est autorisé ici Cliquez pour intégrer Cliquez pour insérer une image + Cliquez pour insérer une macro Légende de l'image... Ecrivez ici... Mises en pages de la Grid @@ -1363,7 +1584,9 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Compositions - Vous n'avez pas ajouté d'onglet + Groupe + Vous n'avez pas ajouté de groupe + Ajouter un groupe Hérité de Ajouter une propriété Label requis @@ -1371,7 +1594,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Configure l'élément de contenu de manière à afficher ses éléments enfants sous forme d'une liste que l'on peut trier et filtrer, les enfants ne seront pas affichés dans l'arborescence 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 + Autoriser comme racine Autorisez les éditeurs à créer du contenu de ce type à la racine de l'arborescence de contenu. Types de noeuds enfants autorisés Autorisez la création de contenu des types spécifiés sous le contenu de ce type-ci @@ -1379,6 +1602,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à 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. + La suppression d'une composition supprimera les données de toutes les propriétés associées. Une fois que vous sauvegardez le type de document, il n'y a plus moyen de faire marche arrière. Editeurs disponibles Réutiliser Configuration de l'éditeur @@ -1407,6 +1631,35 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à l'onglet n'a pas d'ordre de tri Où cette composition est-elle utilisée? Cette composition est actuellement utilisée dans la composition des types de contenu suivants : + Permettre une variation par culture + Permettre aux éditeurs de créer du contenu de ce type dans différentes langues. + Permettre une variation par culture + Type de l'Elément + Est un Type d'Elément + Un Type d'Elément est destiné à être utilisé par exemple dans Nested Content, et pas dans l'arborescence. + Ceci n'est pas d'application pour un Type d'Elément + Vous avez apporté des modifications à cette propriété. Etes-vous certain.e de vouloir les annuler? + + + Ajouter une langue + Langue obligatoire + Les propriétés doivent être remplies dans cette langue avant que le noeud ne puisse être publié. + Langue par défaut + Un site Umbraco ne peut avoir qu'une seule langue par défaut définie. + Changer la langue par défaut peut amener à ce que du contenu par défaut soit manquant. + Retombe sur + Pas de langue alternative + Pour permettre à un site multi-langue de retomber sur une autre langue dans le cas où il n'existe pas dans la langue demandée, sélectionnez-là ici. + Langue alternative + aucune + + + Ajouter un paramètre + Modifier le paramètre + Introduire le nom de la macro + Paramètres + Définir les paramètres qui devraient être disponibles lorsque l'on utilise cette macro. + Sélectionner le fichier de vue partielle de la macro Fabrication des modèles @@ -1427,7 +1680,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à 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; + Remplace les sauts de ligne avec des balises 'br' Champs particuliers Oui, la date seulement Format et encodage @@ -1449,7 +1702,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Champs standards Majuscules Encode pour URL - Formatera les caractères spéciaux de manière à ce qu'ils soient reconnus dans une URL + Formatera les caractères spéciaux dans les URL Sera seulement utilisé si toutes les valeurs des champs ci-dessus sont vides Ce champ sera utilisé seulement si le champ initial est vide Oui, avec l'heure. Séparateur: @@ -1519,14 +1772,17 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Scripts Feuilles de style Modèles + Visualisation des Log Utilisateurs - Analytique + Configuration + Modélisation + Parties Tierces Nouvelle mise à jour disponible %0% est disponible, cliquez ici pour télécharger Aucune connexion au serveur - Erreur lors de la recherche de mises à jour. Veuillez vérifier le stack trace pour obtenir plus d'informations sur l'erreur. + Erreur lors de la recherche de mises à jour. Veuillez vérifier le stack trace pour obtenir plus d'informations. Accès @@ -1538,10 +1794,12 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Changer le mot de passe Changer la photo Nouveau mot de passe + Plus que %0% caractère(s) minimum! + Il devrait y avoir au moins %0% caractère(s) spéciaux. n'a pas été bloqué Le mot de passe n'a pas été modifié Confirmez votre nouveau mot de passe - Vous pouvez changer votre mot de passe d'accès au Back Office Umbraco en remplissant le formulaire ci-dessous puis en cliquant sur le bouton "Changer le mot de passe" + Vous pouvez changer votre mot de passe d'accès au backoffice Umbraco en remplissant le formulaire ci-dessous puis en cliquant sur le bouton "Changer le mot de passe" Canal de contenu Créer un autre utilisateur Créer de nouveaux utilisateurs pour leur donner accès à Umbraco. Lors de la création d'un nouvel utilisateur, un mot de passe est généré que vous pouvez partager avec ce dernier. @@ -1554,7 +1812,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Voir le profil de l'utilisateur Ajouter des groupes pour donner les accès et permissions Inviter un autre utilisateur - Inviter de nouveaux utilisateurs pour leur donner accès à Umbraco. Un email d'invitation sera envoyé à chaque utilisateur avec des informations concernant la connexion à Umbraco. + Inviter de nouveaux utilisateurs pour leur donner accès à Umbraco. Un email d'invitation sera envoyé à chaque utilisateur avec des informations concernant la connexion à Umbraco. Les invitations sont valables pendant 72 heures. Langue Spécifiez la langue dans laquelle vous souhaitez voir les menus et dialogues Date du dernier bloquage @@ -1606,7 +1864,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à a été invité Une invitation a été envoyée au nouvel utilisateur avec les détails concernant la connexion à Umbraco. Bien le bonjour et bienvenue dans Umbraco! Vous serez prêt.e dans moins d'1 minute, vous devez encore simplement configurer votre mot de passe et ajouter une photo pour votre avatar. - Chargez une photo afin que les autres utilisateurs puissent vous reconnaître facilement. + Bienvenue dans Umbraco! Malheureusement, votre invitation a expiré. Veuillez contacter votre administrateur et demandez-lui de vous l'envoyer à nouveau. + Chargez une photo afin que les autres utilisateurs puissent vous reconnaître facilement. Cliquez sur le cercle ci-dessus pour charger votre photo. Rédacteur Modifier Votre profil @@ -1672,7 +1931,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à - + Cliquez sur ce lien pour accepter l'invitation @@ -1712,15 +1971,28 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Nouvel envoi de l'invitation en cours... Supprimer l'Utilisateur Etes-vous certain(e) de vouloir supprimer le compte de cet utilisateur? + Tous + Actif + Désactivé + Bloqué + Invité + Inactif + Nom (A-Z) + Nom (Z-A) + Plus récent + Plus ancien + Dernière connexion Validation Valider comme email Valider comme nombre - Valider comme Url + Valider comme URL ...ou introduisez une validation spécifique Champ obligatoire + Introduisez un message d'erreur de validation personnalisé (optionnel) Introduisez une expression régulière + Introduisez un message d'erreur de validation personnalisé (optionnel) Vous devez ajouter au moins Vous ne pouvez avoir que éléments @@ -1728,6 +2000,12 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Date non valide Pas un nombre Email non valide + La valeur ne peut pas être null + La valeur ne peut pas être vide + Valeur non valide, elle ne correspond pas au modèle correct + Validation personnalisée + %1% supplémentaires.]]> + %1% en trop.]]> - Try Skip IIS Custom foutmeldingen is ingesteld op '%0%'. IIS versie '%1%' wordt gebruikt. - Try Skip IIS Custom foutmeldingen is ingesteld op '%0%'. Het wordt voor de gebruikte IIS versie (%2%) aangeraden deze in te stellen op '%1%'. - Try Skip IIS Custom foutmeldingen ingesteld op '%0%'. + trySkipIisCustomErrors is ingesteld op '%0%'. IIS versie '%1%' wordt gebruikt. + trySkipIisCustomErrors is ingesteld op '%0%'. Het wordt voor de gebruikte IIS versie (%2%) aangeraden deze in te stellen op '%1%'. + trySkipIisCustomErrors ingesteld op '%0%'. Het volgende bestand bestaat niet: '%0%'. '%0%' kon niet gevonden worden in configuratie bestand '%1%'.]]> Er is een fout opgetreden. Bekijk de log file voor de volledige fout: %0%. + Database - Het database schema is correct voor deze versie van Umbraco + %0% problemen zijn gevonden met het databaseschema (Controleer het logboek voor details) + Enkele fouten zijn gevonden tijdens het valideren van het databaseschema tegen de huidige versie van Umbraco. Het cerficaat van de website is ongeldig. Cerficaat validatie foutmelding: '%0%' + Het SSL certificaat van de website is vervallen. + Het SSL certificaat van de website zal vervallen binnen %0% dagen. Fout bij pingen van URL %0% - '%1%' De site wordt momenteel %0% bekeken via HTTPS. De appSetting 'Umbraco.Core.UseHttps' in web.config staat op 'false'. Indien HTTPS gebruikt wordt moet deze op 'true' staan. @@ -1350,57 +1959,407 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Trace mode staat uit. Trace mode staat momenteel aan. Wij raden aan deze instelling uit te zetten voor livegang. Trace mode uitgezet. - Alle mappen hebben de juiste permissie-instellingen! + Alle mappen hebben de juiste rechten. + --> %0%.]]> %0%. Als deze niet in gebruik zijn voor deze omgeving hoeft er geen actie te worden ondernomen.]]> - All files have the correct permissions set. + Alle bestanden hebben de juiste rechten. - %0%.]]> - %0%. Als deze niet in gebruik zijn voor deze omgeving hoeft er geen actie te worden ondernomen.]]> + %0%.]]> + %0%. Als deze niet in gebruik zijn voor deze omgeving hoeft er geen actie te worden ondernomen.]]> X-Frame-Options header of meta-tag om IFRAMEing door andere websites te voorkomen is aanwezig!]]> X-Frame-Options header of meta-tag om IFRAMEing door andere websites te voorkomen is NIET aanwezig.]]> Voorkom IFRAMEing via web.config Voegt de instelling toe aan de httpProtocol/customHeaders section in web.config om IFRAMEing door andere websites te voorkomen. De instelling om IFRAMEing door andere websites te voorkomen is toegevoegd aan de web.config! Web.config kon niet aangepast worden door error: %0% - - %0%.]]> - Er zijn geen headers gevonden die informatie prijsgeven over de gebruikte website technologie! + %0%.]]> + Er zijn geen headers gevonden die informatie vrijgeven over de gebruikte website technologie! In de Web.config werd system.net/mailsettings niet gevonden In de Web.config sectie system.net/mailsettings is de host niet geconfigureerd. SMTP instellingen zijn correct ingesteld en werken zoals verwacht. De SMTP server geconfigureerd met host '%0%' en poort '%1%' kon niet gevonden worden. Controleer of de SMTP instellingen in Web.config file system.net/mailsettings correct zijn. - %0%.]]> - %0%.]]> + %0%.]]> + %0%.]]> +

Resultaten van de geplande Umbraco Health Checks uitgevoerd op %0% op %1%:

%2%]]>
+ Umbraco Health Check Status: %0% + Alle groepen controleren + Groep controleren + + De health checker evalueert verschillende delen van de website voor best practice instellingen, configuratie, mogelijke problemen, enzovoort. U kunt problemen eenvoudig oplossen met een druk op de knop. + U kunt uw eigen health checks toevoegen, kijk even naar de documentatie voor meer informatie over aangepaste health checks.

+ ]]> +
- URL tracker uitzetten - URL tracker aanzetten + URL tracker uitschakelen + URL tracker inschakelen + Cultuur Originele URL Doorgestuurd naar + Redirect Url Beheer + De volgende URLs verwijzen naar dit content item: Er zijn geen redirects Er wordt automatisch een redirect aangemaakt als een gepubliceerde pagina hernoemd of verplaatst wordt. Weet je zeker dat je de redirect van '%0%' naar '%1%' wilt verwijderen? Redirect URL verwijderd. Fout bij verwijderen redirect URL. + Dit zal de redirect verwijderen Weet je zeker dat je de URL tracker wilt uitzetten? URL tracker staat nu uit. Fout bij het uitzetten van de URL Tracker. Meer informatie kan gevonden worden in de log file. URL tracker staat nu aan. Fout bij het aanzetten van de URL tracker. Meer informatie kan gevonden worden in de log file. + + Geen woordenboekitems om uit te kiezen + + + %0% karakters resterend.]]> + %1% te veel.]]> + Content verwijderd met id : {0} gerelateerd aan aan bovenliggend item met Id: {1} Media verwijderd met id: {0} gerelateerd aan aan bovenliggend item met Id: {1} Kan dit item niet automatisch herstellen - Er is geen 'herstel' relatie gevonden voor dit item. Gebruik de "Verplaats" optie om het manueel terug te zetten - Het item dat u wil herstellen onder ('%0%') zit in de prullenbak. Gebruik de "Verplaats" optie om het manueel terug te zetten + Er is geen locatie waar dit item automatisch kan worden hersteld. U kunt het item handmatig verplaatsen met behulp van de onderstaande boomstructuur. + was hersteld onder + + + Richting + Bovenliggend naar onderliggend + Bidirectioneel + Bovenliggend + Onderliggend + Aantal + Relaties + Gemaakt + Commentaar + Naam + Geen relaties voor dit relatietype. + Relatietype + Relaties + + + Aan de slag + Redirect URL Beheer + Inhoud + Welkom + Examine Beheer + Publicatiestatus + Models Builder + Gezondheidscontrole + Profilering + Aan de slag + Umbraco Forms installeren + + + Terug + Actieve layout: + Spring naar + groep + geslaagd + Waarschuwing + mislukt + suggestie + Controle geslaagd + Controle mislukt + Backoffice zoeken openen + Backoffice help openen/sluiten + Jouw profiel opties openen/sluiten + Cultuur en Hostnamen instellen voor %0% + Nieuwe node aanmaken onder %0% + Openbare toegang instellen op %0% + Rechten instellen op %0% + Sorteervolgorde wijzigen voor %0% + Maak een Inhoudssjabloon op basis van %0% + Open context menu voor + Huidige taal + Taal wijzigen naar + Map aanmaken + Partial View + Partial View Macro + Lid + Datatype + Zoeken in het redirect dashboard + Zoeken in de gebruikersgroep sectie + Zoeken in de gebruikers sectie + Item aanmaken + Aanmaken + Bewerken + Naam + Rij toevoegen + Bekijk meer opties + Vertaling aanwezig + Vertaling ontbreekt + Woordenboek items + + + Referenties + Dit Datatype heeft geen referenties. + Gebruikt in Documenttypes + Geen referenties naar Documenttypes. + Gebruikt in Mediatypes + Geen referenties naar Mediatypes. + Gebruikt in Ledentypes + Geen referenties naar Ledentypes. + Gebruikt door + Gebruikt in Documenten + Gebruikt in Leden + Gebruikt in Media + + + Opgeslagen zoekopdracht verwijderen + Log Niveaus + Selecteer alles + Deselecteer alles + Opgeslagen Zoekopdrachten + Zoekopdracht opslaan + Enter a friendly name for your search query + Zoekopdracht filteren + Aantal items + Tijdstempel + Niveau + Machine + Bericht + Uitzondering + Eigenschappen + Zoeken Met Google + Dit bericht met Google opzoeken + Zoeken Met Bing + Dit bericht met Bing opzoeken + Zoeken in Our Umbraco + Search this message on Our Umbraco forums and docs + Our Umbraco met Google doorzoeken + Our Umbraco forums met Google doorzoeken + Umbraco broncode doorzoeken + Zoeken in Umbraco broncode op Github + Umbraco Issues doorzoeken + Umbraco Issues op Github doorzoeken + Zoekopdracht verwijderen + Logs met Request ID zoeken + Logs met Namespace zoeken + Logs met Machine Naam zoeken + Openen + Peilen + Elke 2 seconden + Elke 5 seconden + Elke 10 seconden + Elke 20 seconden + Elke 30 seconden + Elke 2s peilen + Elke 5s peilen + Elke 10s peilen + Elke 20s peilen + Elke 30s peilen + + + Kopieer %0% + %0% van %1% + Alle items verwijderen + Klembord leegmaken + + + Eigenschapsacties openen + Eigenschapsacties sluiten + + + Wachten + Status vernieuwen + Geheugencache + + + + Vernieuwen + Database Cache + + Opnieuw bouwen kan duur zijn. + Gebruik het wanneer herladen niet genoeg is en u denkt dat de databasecache niet correct + is gegenereerd—wat zou duiden op een kritiek Umbraco-probleem. + ]]> + + Opnieuw bouwen + Interne onderdelen + + niet te gebruiken. + ]]> + + Verzamelen + Gepubliceerde Cachestatus + Caches + + + Prestatieprofilering + + + Umbraco wordt uitgevoerd in de foutopsporingsmodus. Dit betekent dat u de ingebouwde prestatieprofiler kunt gebruiken om de prestaties te beoordelen bij het renderen van pagina's. +

+

+ Als je de profiler voor een specifieke paginaweergave wilt activeren, voeg je umbDebug=true toe aan de querystring wanneer je de pagina opvraagt. +

+

+ Als je wil dat de profiler standaard wordt geactiveerd voor alle paginaweergaven, kun je de onderstaande schakelaar gebruiken. + Het plaatst een cookie in je browser, die vervolgens de profiler automatisch activeert. + Met andere woorden, de profiler zal alleen voor jouw browser actief zijn, niet voor andere bezoekers. +

+ ]]> +
+ Activeer de profiler standaard + Vriendelijke herinnering + + + Je mag een productiesite nooit in de foutopsporingsmodus laten uitvoeren. Je kan de foutopsporingsmodus uitschakelen door de instelling debug="false" uit het <compilation /> element te verwijderen in het web.config bestand. +

+ ]]> +
+ + + Umbraco wordt op dit ogenblik niet uitgevoerd in de foutopsporingsmodus, dus je kan de ingebouwde profiler niet gebruiken. Dit is hoe het zou moeten zijn voor een productiewebsite. +

+

+ De foutopsporingsmodus wordt ingeschakeld door debug="true" toe te voegen in het <compilation /> element in web.config. +

+ ]]> +
+ + + Je bent slechts een klik verwijderd van uren aan Umbraco trainingvideo's. + + Wil je Umbraco onder de knie krijgen? Besteed een paar minuten aan het leren van enkele best practices door een van deze video's over het gebruik van Umbraco te bekijken. Bezoek umbraco.tv voor meer Umbraco videos

+ ]]> +
+ Om je op weg te helpen + + + Start hier + Deze sectie bevat de bouwstenen voor jouw Umbraco-site. Volg de onderstaande links voor meer informatie over het werken met de items in de sectie Instellingen + Meer te weten komen + + in het Documentatiegedeelte van Our Umbraco + ]]> + + + Community Forum + ]]> + + + instructievideo's (sommige zijn gratis, andere vereisen een abonnement) + ]]> + + + productiviteitsverhogende programma's en commerciële ondersteuning + ]]> + + + training en certificering opportuniteiten + ]]> + + + + Welkom bij Het Vriendelijke CMS + Bedankt om voor Umbraco te kiezen - We denken dat dit het begin van iets moois is. Hoewel het in het begin misschien overweldigend aanvoelt, hebben we er veel aan gedaan om de leercurve zo soepel en snel mogelijk te laten verlopen. + + + Umbraco Forms + Maak formulieren met behulp van een intuïtieve interface. Van eenvoudige contactformulieren die e-mails versturen tot geavanceerde vragenlijsten die integreren met CRM-systemen. Je klanten zullen er dol op zijn! + + + Nieuwe blok aanmaken + Instellingensectie toevoegen + Weergave selecteren + Stylesheet selecteren + Miniatuur kiezen + Nieuwe aanmaken + Aangepaste stylesheet + Stylesheet toevoegen + Editor uiterlijk + Data modellen + Catalogus uiterlijk + Achtergrondkleur + Icoon kleur + Inhoud model + Label + Aangepaste weergave + Aangepaste weergave-omschrijving tonen + Overschrijf hoe dit blok wordt weergegeven in de BackOffice-gebruikersinterface. Kies een .html-bestand met je presentatie. + Instellingen model + Grootte van overlay-editor + Aangepaste weergave toevoegen + Instellingen toevoegen + Label sjabloon overschrijven + %0% wil verwijderen?]]> + %0% wil verwijderen?]]> + De inhoud van dit blok is nog steeds aanwezig, bewerken van deze inhoud is niet langer mogelijk en wordt weergegeven als niet-ondersteunde inhoud. + + Miniatuur + Miniatuur toevoegen + Lege aanmaken + Klembord + Instellingen + Geavanceerd + Inhoudseditor geforceerd verbergen + Je hebt aanpassingen gemaakt aan deze inhoud. Wil je deze wijzigingen verwerpen? + Wijzigingen opslaan? + + Fout! + Het Elementtype van dit blok bestaat niet meer + Inhoud toevoegen + Eigenschap '%0%' gebruikt editor '%1%' die niet ondersteund wordt in blokken. + + + Wat zijn Inhoudssjablonen? + Inhoudssjablonen is vooraf gedefinieerde inhoud die kan worden geselecteerd bij het maken van een nieuwe node. + Hoe maak ik een Inhoudssjabloon? + + Er zijn 2 manieren om Inhoudssjablonen te maken:

+
    +
  • Klik met de rechtermuisknop op een inhoudsnode en selecteer "Inhoudssjabloon aanmaken" om een nieuwe Inhoudssjabloon te maken.
  • +
  • Klik met de rechtermuisknop op Inhoudssjablonen in de boomstructuur in de sectie Instellingen en selecteer het documenttype waarvoor je een Inhoudssjabloon wilt maken.
  • +
+

Nadat de Inhoudssjabloon een naam heeft, kunnen redacteuren ze gaan gebruiken als basis voor hun nieuwe pagina.

+ ]]> +
+ Hoe beheer ik Inhoudssjablonen? + U kunt Inhoudssjablonen bewerken en verwijderen vanuit de boomstructuur "inhoudssjablonen" in de sectie Instellingen. Vouw het documenttype uit waarop de Inhoudssjabloon is gebaseerd en klik erop om het te bewerken of te verwijderen. diff --git a/TestSite/Umbraco/Config/Lang/pl.xml b/TestSite/Umbraco/Config/Lang/pl.xml index de3e988..806f3ba 100644 --- a/TestSite/Umbraco/Config/Lang/pl.xml +++ b/TestSite/Umbraco/Config/Lang/pl.xml @@ -121,7 +121,7 @@ Zapisz i publikuj Zapisz i wyślij do zaakceptowania Zapisz widok listy - Podgląd + Podgląd Podgląd jest wyłączony, ponieważ żaden szablon nie został przydzielony Wybierz styl Pokaż style @@ -130,26 +130,6 @@ 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 @@ -205,7 +185,7 @@ Elementy dzieci Cel Oznacza to następującą godzinę na serwerze: - Co to oznacza?]]> + Co to oznacza?]]> Czy na pewno chcesz usunąć ten element? Właściwość %0% używa edytora %1%, który nie jest wspierany przez Nested Content. Dodaj kolejne pole tekstowe @@ -321,8 +301,8 @@ 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 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' + 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 @@ -612,8 +592,8 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb

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".

]]> + 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.]]> @@ -660,7 +640,7 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Chcę zacząć od zera dowiedz się jak) + (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? @@ -728,7 +708,7 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Zaloguj się poniżej Zaloguj się z Sesja wygasła - © 2001 - %0%
umbraco.com

]]>
+ © 2001 - %0%
umbraco.com

]]>
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 @@ -737,7 +717,7 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb 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%

]]>
+ Twoja nazwa użytkownika do zalogowania się w Umbraco backoffice to: %0%

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

%1%

]]>
Panel zarządzania @@ -981,7 +961,6 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb 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ść. Dodaj ikonę @@ -1096,9 +1075,9 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb ]]> Nazwa Sekcji Sekcja jest wymagana - + @section, w przeciwnym przypadku wystąpi błąd. - + ]]> Konstruktor zapytań Element zwrócony, w Chcę @@ -1321,7 +1300,7 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Zmień hasło! 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" + Możesz zmienić swoje hasło w Umbraco backoffice przez wypełnienie formularza poniżej i kliknięcie przycisku "Zmień hasło" Kanał zawartości Opis Wyłącz użytkownika @@ -1481,4 +1460,8 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb pozostało znaków + + Zaznacz wszystko + Odznacz wszystkie + diff --git a/TestSite/Umbraco/Config/Lang/pt.xml b/TestSite/Umbraco/Config/Lang/pt.xml index e7afd04..90ad2c9 100644 --- a/TestSite/Umbraco/Config/Lang/pt.xml +++ b/TestSite/Umbraco/Config/Lang/pt.xml @@ -67,7 +67,7 @@ Salvar Salvar e publicar Salvar e mandar para aprovação - Prévia + Prévia Escolha estilo Mostrar estilos Inserir tabela @@ -180,8 +180,8 @@ Os itens na lixeira agora estão sendo removidos. Favor não fechar esta janela enquanto este processo é concluído A lixeira agora está vazia Quando itens são removidos da lixeira estes somem para sempre - regexlib.com está no momento sofrendo dificuldades dos quais não temos controle. Pedimos desculpas pela inconveniência.]]> - Busque por uma expressão regular para adicionar validação à um campo de formulário. Exemplo: 'email', 'zip-code' (código postal), 'url' + regexlib.com está no momento sofrendo dificuldades dos quais não temos controle. Pedimos desculpas pela inconveniência.]]> + Busque por uma expressão regular para adicionar validação à um campo de formulário. Exemplo: 'email', 'zip-code' (código postal), 'URL' Remover Macro Campo obrigatório Site foi re-indexado @@ -324,7 +324,7 @@ Atualizar Atualizar Subir (Upload) - Url + URL Usuário Usuário Valor @@ -354,8 +354,8 @@ Próximo para prosseguir.]]> Banco de dados não encontrado! Favor checar se a informação no "connection string" do "web.config" esteja correta.

Para prosseguir, favor editar o arquivo "web.config" (usando Visual Studio ou seu editor de texto favorito), role até embaixo, adicione a connection string para seu banco de dados com a chave de nome "UmbracoDbDSN" e salve o arquivo

-

Clique o botão tentar novamente quando terminar.
- Mais informações em como editar o web.config aqui.

]]>
+

Clique o botão tentar novamente quando terminar.
+ Mais informações em como editar o web.config aqui.

]]> Favor contatar seu provedor de internet ou hospedagem web se necessário. Se você estiver instalando em uma máquina ou servidor local é possível que você precise dessas informações por um administrador de sistema.]]> Pressione o botão atualizar para atualizar seu banco de dados para Umbraco %0%

@@ -394,7 +394,7 @@ Para correr Umbraco você vai precisar atualizar as configurações de permissõ Também guarda informações temporárias (cache) para melhorar a performance do seu website.]]>
Eu quero começar do zero learn how) + (learn how) Você ainda pode escolher instalar Runway mais tarde. Favor ir à seção Desenvolvedor e selecione pacotes.]]> Você acabou de configurar uma plataforma Umbraco limpa. O que deseja fazer a seguir? Runway está instalado @@ -445,7 +445,7 @@ Pressione "próximo" para iniciar o assistente.]]> Renovar agora para salvar seu trabalho - © 2001 - %0%
umbraco.com

]]>
+ © 2001 - %0%
umbraco.com

]]>
Painel @@ -835,4 +835,8 @@ Você pode publicar esta página e todas suas sub-páginas ao selecionar pub Tipos de usuários Escrevente + + Selecionar tudo + Desmarcar todos + diff --git a/TestSite/Umbraco/Config/Lang/ru.xml b/TestSite/Umbraco/Config/Lang/ru.xml index c52e17e..e078ef1 100644 --- a/TestSite/Umbraco/Config/Lang/ru.xml +++ b/TestSite/Umbraco/Config/Lang/ru.xml @@ -155,7 +155,7 @@ Направить на публикацию Сохранить список Выбрать - Предварительный просмотр + Предварительный просмотр Предварительный просмотр запрещен, так как документу не сопоставлен шаблон Другие действия Выбрать стиль @@ -163,27 +163,6 @@ Вставить таблицу Отменить - - Чтобы сменить тип документа для выбранного узла, сначала выберите тип из списка разрешенных для данного расположения. - Затем подтвердите и/или исправьте сопоставление свойств текущего типа документа свойствам нового и нажмите "Сохранить". - Документ переопубликован. - Текущее свойство - Текущий тип - Тип документа не может быть изменен, так как для данного расположения нет разрешенных альтернатив. - Альтернативный тип станет доступным, если его разрешить как тип, пригодный для создания дочерних узлов внутри родительского узла данного документа. - Тип документа изменен - Сопоставление свойств - Сопоставлено свойству - Новый шаблон - Новый тип - нет - Узел - Выберите новый тип документа - Тип документа выбранного узла успешно изменен на [new type] и следующие свойства были перенесены: - в - Невозможно закончить перенос свойств, так как одно или более свойства имеют больше чем одно сопоставление. - Показаны только допустимые для данного расположения альтернативные типы. - Вы не указали ни одного допустимого цвета @@ -241,7 +220,7 @@ Очистить дату ВНИМАНИЕ: этот документ опубликован, но его URL вступает в противоречие с документом %0% Это время будет соответствовать следующему времени на сервере: - Что это означает?]]> + Что это означает?]]> Задать дату Порядок сортировки обновлен Для сортировки узлов просто перетаскивайте узлы или нажмите на заголовке столбца. Вы можете выбрать несколько узлов, удерживая клавиши "shift" или "ctrl" при пометке @@ -371,8 +350,8 @@ Все элементы в корзине сейчас удаляются. Пожалуйста, не закрывайте это окно до окончания процесса удаления Корзина пуста Вы больше не сможете восстановить элементы, удаленные из корзины - regexlib.com испытывает в настоящее время некоторые трудности, не зависящие от нас. Просим извинить за причиненные неудобства.]]> - Используйте поиск регулярных выражений для добавления сервиса проверки к полю Вашей формы. Например: 'email, 'zip-code', 'url' + regexlib.com испытывает в настоящее время некоторые трудности, не зависящие от нас. Просим извинить за причиненные неудобства.]]> + Используйте поиск регулярных выражений для добавления сервиса проверки к полю Вашей формы. Например: 'email, 'zip-code', 'URL' Удалить макрос Обязательное поле Сайт переиндексирован @@ -783,8 +762,7 @@

Для настройки откройте файл "web.config" с помощью любого текстового редактора и добавьте нужную информацию в строку подключения (параметр "UmbracoDbDSN"), затем сохраните файл.

Нажмите кнопку "Повторить" когда все будет готово
- Более подробно о внесении изменений в файл "web.config" рассказано здесь.

]]> + Более подробно о внесении изменений в файл "web.config" рассказано здесь.

]]> Пожалуйста, свяжитесь с Вашим хостинг-провайдером, если есть необходимость, а если устанавливаете на локальную рабочую станцию или сервер, то получите информацию у Вашего системного администратора.]]> @@ -831,7 +809,7 @@ Здесь можно узнать об этом подробнее) Вы также можете отложить установку "Runway" на более позднее время. Перейдите к разделу "Разработка" и выберите пункт "Пакеты". + (Здесь можно узнать об этом подробнее) Вы также можете отложить установку "Runway" на более позднее время. Перейдите к разделу "Разработка" и выберите пункт "Пакеты". ]]> Вы только что установили чистую платформу Umbraco. Какой шаг будет следующим? "Runway" установлен @@ -888,7 +866,7 @@ Обновите сейчас, чтобы сохранить сделанные изменения - © 2001 - %0%
umbraco.com

]]>
+ © 2001 - %0%
umbraco.com

]]>
Сегодня же выходной! Понедельник — день тяжелый... Вот уже вторник... @@ -954,7 +932,7 @@ - + Нажмите на эту ссылку для того, чтобы сбросить пароль @@ -1093,7 +1071,7 @@
- ВНЕСТИ ИЗМЕНЕНИЯ
+ ВНЕСТИ ИЗМЕНЕНИЯ @@ -1328,7 +1306,6 @@ Добавить значок - в качестве родительского типа. Вкладки родительского типа не показаны и могут быть изменены непосредственно в родительском типе Родительский тип контента разрешен Данный тип контента использует Шаблон по-умолчанию @@ -1492,9 +1469,9 @@ ]]> Название секции Секция обязательна - + @section, в противном случае генерируется ошибка. - + ]]> Генератор запросов элементов в результате, за Мне нужны @@ -1669,7 +1646,7 @@ Добавьте пользователя в группу(ы) для задания прав доступа Пригласить Приглашение в панель администрирования Umbraco -

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

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

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

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

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

%3%

]]>

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

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

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

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

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

%3%

]]> @@ -1724,7 +1701,7 @@ - + Нажмите на эту ссылку, чтобы принять приглашение @@ -1829,7 +1806,7 @@ Валидация Валидация по формату email Валидация числового значения - Валидация по формату Url + Валидация по формату URL ...или указать свои правила валидации Обязательно к заполнению Задайте регулярное выражение @@ -1841,4 +1818,8 @@ Не является числом неверный формат email-адреса + + Выбрать все + Убрать выделение со всего + diff --git a/TestSite/Umbraco/Config/Lang/sv.xml b/TestSite/Umbraco/Config/Lang/sv.xml index 152a40b..1c4134f 100644 --- a/TestSite/Umbraco/Config/Lang/sv.xml +++ b/TestSite/Umbraco/Config/Lang/sv.xml @@ -134,33 +134,13 @@ Spara och skicka för godkännande Schemaläggning Välj - Förhandsgranska + Förhandsgranska Förhandsgranskning är avstängt på grund av att det inte finns någon mall tilldelad - Ångra + Gör något annat Välj stil Visa stil Infoga tabell - - För att ändra vald dokumenttyp, välj först i listan av giltiga typer för denna platsen. - Konfirmera sedan och/eller ändra mappningen av egenskaper från aktuell dokumenttyp till den nya, och klicka sedan på spara. - Innehållet har blivit publicerat på nytt - Aktuell egenskap - Aktuell typ - Dokumenttypen kan inte ändras, eftersom det inte finns några giltiga val för denna plats. - Dokumenttypen är ändrad - Mappningsegenskaper - Mappning till egenskap - Ny sidmall - Ny egenskap - ingen - Innehåll - Välj ny dokumenttyp - Dokumenttypen på valt innehåll har ändrats till [new type] utan problem och följande egenskaper är mappade: - till - Kunde inte slutföra egenskapsmappningen då en eller flera egenskaper har en eller flera mappningar definierade. - Enbart giltiga alternativa egenskaper visas för platsen - Du har inte konfigurerat några giltiga färger @@ -272,6 +252,8 @@ Kopierade %0% av %1% objekt + Länktitel + Länk Namn Hantera domännamn Stäng fönstret @@ -300,15 +282,17 @@ Allt som ligger i papperskorgen tas nu bort. Stäng inte detta fönster förrän detta är klart Papperskorgen är nu tom Om du tömmer papperskorgen kommer allt som ligger i den att tas bort permanent - regexlib.com's webbtjänst har för närvarande driftsstörningar. Tyvärr kan vi inte göra något åt detta.]]> - Sök efter en regular expression som kan validera ett formulärsfält. t.ex. 'email' eller 'url' + regexlib.com's webbtjänst har för närvarande driftsstörningar. Tyvärr kan vi inte göra något åt detta.]]> + Sök efter en regular expression som kan validera ett formulärsfält. t.ex. 'email' eller 'URL' Ta bort makro Obligatoriskt formulärsfält Webbplatsen har indexerats Cache för webbplatsen har uppdaterats. Allt publicerat innehåll är nu uppdaterat. Innehåll som inte har publicerats är fortfarande opublicerat. Webbplatsens cache kommer att uppdateras. Allt innehåll som är publicerat kommer att uppdateras. Innehåll som inte är publicerat kommer att förbli opublicerat. Välj startnod för innehåll + Välj media Välj ikon + Välj länk Välj startnod för media Välj användargrupper Välj sektioner @@ -318,6 +302,9 @@ Klicka på förhandsgranskningsbilden för att se bilden i full storlek Välj ett objekt Se cachat objekt + Länk till sida + Öppnar länken i ett nytt fönster eller flik + Länk till media Redigera de olika översättningarna för ordboksinlägget %0% nedan. Du kan lägga till ytterligare språk under 'språk' i menyn till vänster. @@ -504,7 +491,7 @@ Databaskonfiguration installera]]> Nästa för att fortsätta.]]> - Databasen kunde inte hittas! Kontrollera att informationen i databasanslutnings-inställningarna i filen "web.config" är rätt.

För att fortsätta måste du redigera filen "web.config" (du kan använda Visual Studio eller din favorit text-redigerare), bläddra till slutet, lägg till databasanslutnings-inställningarna för din databas i fältet som heter "umbracoDbDSN" och spara filen.

Klicka på Försök igen knappen när du är klar.
> Mer information om att redigera web.config hittar du här.

]]>
+ Databasen kunde inte hittas! Kontrollera att informationen i databasanslutnings-inställningarna i filen "web.config" är rätt.

För att fortsätta måste du redigera filen "web.config" (du kan använda Visual Studio eller din favorit text-redigerare), bläddra till slutet, lägg till databasanslutnings-inställningarna för din databas i fältet som heter "umbracoDbDSN" och spara filen.

Klicka på Försök igen knappen när du är klar.
Mer information om att redigera web.config hittar du här.

]]>
Eventuellt kan du behöva kontakta ditt webb-hotell. Om du installerar på en lokal maskin eller server kan du få informationen från din systemadministratör.]]> Tryck Uppgradera knappen för att uppgradera din databas till Umbraco %0%

Du behöver inte vara orolig. Inget innehåll kommer att raderas och efteråt kommer allt att fungera som vanligt!

]]>
Tryck Nästa för att fortsätta.]]> @@ -532,7 +519,7 @@ Konfigurerar mapprättigheter Umbraco behöver skriv/ändra rättigheter till vissa mappar för att spara filer som bilder och PDFer. Umbraco sparar också temporär data (så kallad cache) för att öka prestandan på din webbplats. Jag vill börja från början - lär dig hur) Du kan fortfarande välja att installera Runway senare. Gå in i Utvecklarsektionen och välj Paket.]]> + lär dig hur) Du kan fortfarande välja att installera Runway senare. Gå in i Utvecklarsektionen och välj Paket.]]> Du har just installerat en ren Umbraco platform. Vad vill du göra härnäst? Runway är installerat Det här är vår lista över rekommenderade moduler, markera de moduler du vill installera, eller visa den fullständiga listan]]> @@ -551,7 +538,7 @@ Umbraco %0% är installerat och klart för användning /web.config filen och ändra AppSettingsnyckeln UmbracoConfigurationStatus på slutet till %0%]]> börja omedelbart genom att klicka på "Starta Umbraco"-knappen nedan.
Om du är en ny Umbraco användarekan du hitta massor av resurser på våra kom igång sidor.]]>
- Starta Umbraco För att administrera din webbplats öppnar du bara Umbraco back office och börjar lägga till innehåll, uppdatera mallar och stilmallar eller lägga till nya funktioner.]]> + Starta Umbraco För att administrera din webbplats öppnar du bara Umbraco backoffice och börjar lägga till innehåll, uppdatera mallar och stilmallar eller lägga till nya funktioner.]]> Anslutningen till databasen misslyckades. Se Umbraco %0% antingen för en ny installation eller en uppgradering från version 3.0.

Tryck på "next" för att börja.]]>
@@ -567,7 +554,7 @@ Förnya nu för att spara ditt arbete - © 2001 - %0%
umbraco.com

]]>
+ © 2001 - %0%
umbraco.com

]]>
Happy super Sunday Happy manic Monday Happy tremendous Tuesday @@ -589,6 +576,11 @@ eller klicka här för att välja filer Drag och släpp dina filer i denna yta + + Skapa en ny medlem + Alla medlemmar + Medlemsgrupper har inga extra egenskaper för redigering. + Välj sida ovan... %0% har kopierats till %1% @@ -720,7 +712,6 @@ Användare - som huvudinnehållstyp. Tabbar från huvudinnehållstyper visas inte och kan endast redigeras på själva huvudinnehållstypen. Huvudinnehållstyp påslagen Denna huvudinnehållstyp använder Defaultmall @@ -864,7 +855,7 @@ Standardfält Versaler URL-koda - Om fältets innehåll skall sändas till en url, skall detta slås på så att specialtecken kodas + Om fältets innehåll skall sändas till en URL, skall detta slås på så att specialtecken kodas Texten kommer användas om ovanstående fält är tomma Fältet kommer användas om det primära fältet ovan är tomt Ja, med tid. Separator: @@ -935,7 +926,7 @@ Ändra lösenord Ändra bild Bekräfta det nya lösenordet - Du kan byta ditt lösenord för Umbraco Back Office genom att fylla i nedanstående formulär och klicka på knappen "Ändra lösenord". + Du kan byta ditt lösenord för Umbraco backoffice genom att fylla i nedanstående formulär och klicka på knappen "Ändra lösenord". Innehållskanal Skapa en till användare Skapa nya användare för att ge dom åtkomst till Umbraco. När en ny användare skapas kommer ett lösenord genereras som du kan dela med användaren. @@ -1017,4 +1008,8 @@ Äldst Senaste login + + Välj alla + Avmarkera alla + diff --git a/TestSite/Umbraco/Config/Lang/tr.xml b/TestSite/Umbraco/Config/Lang/tr.xml index 02069a5..464b697 100644 --- a/TestSite/Umbraco/Config/Lang/tr.xml +++ b/TestSite/Umbraco/Config/Lang/tr.xml @@ -5,365 +5,755 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Kültür ve Hostnames - Denetim Trail - Düğüm Araştır - Belge Türü Değiştir - Kopya + Kültür ve Ana Bilgisayar Adları + Denetim Yolu + Düğüme Göz At + Belge Türünü Değiştir + Kopyala Oluştur + Dışa Aktar Paket Oluştur + Grup 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 + Ayarları düzenle + Geri dönüşüm kutusunu boşalt + Etkinleştir + Belge Türünü Dışa Aktar + Belge Türünü İçe Aktar + Paketi İçe Aktar + Kanvas'ta Düzenle Çıkış Taşı Bildirimler - Genel Erişim + Genel erişim Yayımla Yayından Kaldır Yeniden Yükle - Siteleri Yeniden Yayınla - Düzelt + Tüm siteyi yeniden yayınla + Kaldır + Yeniden adlandır + Geri Yükle + %0% sayfası için izinleri ayarlayın + Nereye kopyalayacağınızı seçin + Taşınacağınız yeri seçin + aşağıdaki ağaç yapısına + Seçili öğelerin nereye kopyalanacağını seçin + Seçili öğelerin nereye taşınacağını seçin + konumuna taşındı, + kopyalandı, + silindi, İzinler - Rollback - Yayın için Gönder - Çeviri Gönder + Geri al + Yayınlamak İçin Gönder + Çeviriye Gönder + Grup ayarla Sırala - Yayına Gönder Çevir Güncelle - Varsayılan Değer + İzinleri ayarlayın + Kilidi Aç + İçerik Şablonu Oluşturun + Daveti Yeniden Gönder + + + İçerik + Yönetim + Yapı + Diğer + + + Kültür ve ana bilgisayar adları atamak için erişime izin ver + Bir düğümün geçmiş günlüğünü görüntülemek için erişime izin ver + Bir düğümü görüntülemek için erişime izin verin + Bir düğüm için belge türünü değiştirmek üzere erişime izin ver + Bir düğümü kopyalamak için erişime izin ver + Düğüm oluşturmak için erişime izin verin + Düğümleri silmek için erişime izin ver + Bir düğümü taşımak için erişime izin ver + Erişime, bir düğüm için genel erişimi ayarlama ve değiştirme izni verme + Bir düğüm yayınlamak için erişime izin verin + Bir düğümü yayından kaldırmak için erişime izin ver + Bir düğüm için izinleri değiştirmek üzere erişime izin ver + Bir düğümü önceki bir duruma geri döndürmek için erişime izin ver + Yayınlamadan önce onay için bir düğüm gönderme erişimine izin ver + Çeviri için bir düğüm gönderme erişimine izin ver + Düğümler için sıralama düzenini değiştirmek için erişime izin ver + Bir düğümü çevirmek için erişime izin ver + Bir düğümü kaydetme erişimine izin ver + İçerik Şablonu oluşturma erişimine izin verin + + + İçerik + Bilgi İzin reddedildi. - Yeni Domain ekle + Yeni Alan Ekle kaldır - Geçersiz node. - Geçersiz domain biçimi. - Domain zaten eklenmiş. + Geçersiz düğüm. + Bir veya daha fazla alanın biçimi geçersiz. + Etki alanı zaten atanmış. Dil - Domain - Yeni domain '%0%' oluşturuldu - Domain '%0%' silindi - Domain '%0%' zaten atanmış - Domain '%0%' güncellendi - Geçerli domain düzenle - Miras Al + Alan + Yeni '%0%' etki alanı oluşturuldu + '%0%' alan adı silindi + '%0%' etki alanı zaten atanmış + '%0%' etki alanı güncellendi + Mevcut Etki Alanlarını Düzenle + + + + Devral 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 + + veya kültürü üst düğümlerden devralın. Ayrıca
+ aşağıdaki alan da geçerli değilse geçerli düğüme.]]> +
+ Alanlar + Seçimi temizle Seç - Başka birşey yapın + Başka bir şey yapın Kalın - Paragraf girinti iptal + Paragraf Girintisini İptal Et Form alanı ekle - Grafik başlık ekle - Html Düzenle - Paragraf girintisi - Yatık - Ortalı + Grafik başlığı ekle + Html'yi Düzenle + Paragrafı Girintile + İtalik + Ortala Sola Yasla Sağa Yasla - Link ekle - Yerel bağlantı ekle - Bulet listesi + Bağlantı Ekle + Yerel bağlantı ekle (bağlantı) + Madde İşareti Listesi Sayısal Liste - Macro ekle + Makro ekle Resim ekle - Düzenleme ilişkileri - Listeye Dön + Yayınlayın ve kapatın + Nesillerle yayınlayın + İlişkileri düzenle + 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. + Kaydet ve kapat + Kaydet ve yayınla + Kaydet ve planla + Kaydet ve onaya gönder + Liste görünümünü kaydet + Planla + Önizleme + Kaydet ve önizle + Atanmış şablon olmadığından önizleme devre dışı bırakıldı + Stil seçin + Stilleri göster + Tablo ekle + Modelleri kaydedin ve oluşturun + Geri Al + Yeniden yap + Etiketi sil + İptal + Onayla + Daha fazla yayınlama seçeneği + Gönder + Gönder ve kapat + + + Görüntüleniyor + İçerik silindi + Yayınlanmamış içerik + Kaydedilen ve Yayınlanan İçerik + Diller için kaydedilen ve yayınlanan içerik: %0% + İçerik kaydedildi + Diller için kaydedilen içerik: %0% + İçerik taşındı + İçerik kopyalandı + İçerik geri alındı ​​ + Yayınlanmak için gönderilen içerik + Diller için yayınlanmak üzere gönderilen içerik: %0% + Kullanıcı tarafından gerçekleştirilen alt öğeleri sıralama + %0% + Kopyala + Yayınla + Yayınla + Taşı + Kaydet + Kaydet + Sil + Yayından Kaldır + Geri Al + Yayınlamak İçin Gönder + Yayınlamak İçin Gönder + Sırala + Özel + Geçmiş (tüm varyantlar) + + + %0% kimliğine sahip ebeveyn altında klasör oluşturulamadı + %0% adıyla ebeveyn altında klasör oluşturulamadı + Klasör adı geçersiz karakterler içeremez. + Öğe silinemedi: %0% - Yayımlandı + Yayınlandı Bu sayfa hakkında - takma ad - ( nasıl telefon üzerinden resim anlatırsınız ) - Alternatif Linkler + Takma ad + (resmi telefonda nasıl tarif edersiniz) + Alternatif Bağlantılar 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 + Oluşturan + Orijinal yazar + Güncelleyen + Oluşturuldu + Bu belgenin oluşturulduğu tarih / saat Belge Türü - kurgu - en kaldır - Bu madde yayınlanmasından sonra değiştirildi + Düzenle + Kaldır + Bu öğe yayınlandıktan sonra değiştirildi Bu öğe yayınlanmadı Son yayınlanan + Gösterilecek öğe yok Listede gösterilecek öğe yok. + Hiçbir içerik eklenmedi + Üye eklenmedi Medya Türü - Medya öğesinin bağlantı ( lar) + Medya öğelerine bağlantı Üye Grubu - rol + Rol Üye Türü - Hiçbir tarih seçildi + Hiçbir değişiklik yapılmadı + Tarih seçilmedi Sayfa başlığı + Bu medya öğesinin bağlantısı yok Ö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 + Bu belge yayınlandı, ancak '%0%' üst öğesi yayından kaldırıldığı için görünmüyor + Bu kültür yayınlandı, ancak '%0%' üst öğe üzerinde yayınlanmadığı için görünmüyor + Bu belge yayınlandı, ancak önbellekte yok + URL alınamadı + Bu belge yayınlandı, ancak url'si %0% içeriğiyle çakışacak + Bu belge yayınlandı, ancak url'si yönlendirilemez + Yayınla + Yayınlandı + Yayınlandı (bekleyen değişiklikler) + Yayın Durumu + %0% ve altındaki tüm içerik öğelerini yayınlayın ve böylece içeriğini herkese açık hale getirin.]]> + + Yayınla + Yayından kaldır + Tarihi Temizle + Tarihi ayarla + Sıralayıcı güncellendi + Düğümleri sıralamak için, düğümleri sürükleyin veya sütun başlıklarından birine tıklayın. Seçerken "shift" veya "kontrol" tuşunu basılı tutarak birden fazla düğüm seçebilirsiniz + İstatistikler Başlık (isteğe bağlı) Alternatif metin (isteğe bağlı) - tip - Yayından + Tür + Yayından Kaldır + Yayınlanmadı 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 + Bu belgenin düzenlendiği tarih / saat + Dosyaları kaldırın + Resmi medya öğesinden kaldırmak için burayı tıklayın + Dosyayı medya öğesinden kaldırmak için burayı tıklayın + Belgeye bağlantı + Grup(lar) ın üyesi + Grup(lar) ın üyesi değil + Alt öğeler + Hedef + Bu, sunucuda şu zamana çevrilir: + Ne bu ne anlama geliyor? ]]> + Bu öğeyi silmek istediğinizden emin misiniz? + %0% özelliği,%1% düzenleyiciyi kullanıyor ve bu, Yuvalanmış İçerik tarafından desteklenmiyor. + Tüm öğeleri silmek istediğinizden emin misiniz? + Bu özellik için hiçbir içerik türü yapılandırılmadı. + Öğe türü ekle + Öğe türünü seçin + Özelliklerinin görüntülenmesi gereken grubu seçin. Boş bırakılırsa, eleman türündeki ilk grup kullanılacaktır. + Adını her bir öğeyle karşılaştırmak için açısal bir ifade girin. kullanın + öğe dizinini görüntülemek için + Başka bir metin kutusu ekle + Bu metin kutusunu kaldırın + İçerik Kökü + Yayınlanmamış içerik öğelerini dahil edin. + Bu değer gizlidir. Bu değeri görüntülemek için erişime ihtiyacınız varsa, lütfen web sitesi yöneticinizle iletişime geçin. + Bu değer gizlidir. + Hangi dilleri yayınlamak istersiniz? İçeriği olan tüm diller kaydedilir! + Hangi dilleri yayınlamak istersiniz? + Hangi dilleri kaydetmek istersiniz? + İçeriği olan tüm diller oluşturma sırasında kaydedilir! + Onay için hangi dilleri göndermek istersiniz? + Hangi dilleri planlamak istersiniz? + Yayından kaldırılacak dilleri seçin. Zorunlu bir dilin yayından kaldırılması tüm dilleri yayından kaldırır. + Yayınlanan Diller + Yayınlanmamış Diller + Değiştirilmemiş Diller + Bu diller oluşturulmadı + + Tüm yeni varyantlar kaydedilecektir. + Hangi çeşitleri yayınlamak istersiniz? + Hangi değişkenlerin kaydedileceğini seçin. + Onaya gönderilecek çeşitleri seçin. + Planlanmış yayınlamayı ayarlayın... + Yayından kaldırılacak varyantları seçin. Zorunlu bir dilin yayından kaldırılması tüm değişkenleri yayından kaldıracaktır. + Yayınlamanın gerçekleşmesi için aşağıdaki varyantlar gereklidir: + + Yayınlamaya hazır değiliz + Yayınlamaya hazır mısınız? + Kaydetmeye Hazır mısınız? + Onaya gönder + İçerik öğesini yayınlamak ve / veya yayından kaldırmak için tarih ve saati seçin. + Yeni oluştur + Panodan yapıştır + Bu öğe Geri Dönüşüm Kutusu'nda + + + '%0%' den yeni bir İçerik Şablonu oluşturun + Boş + Bir İçerik Şablonu Seçin + İçerik Şablonu oluşturuldu + '%0%' üzerinden bir İçerik Şablonu oluşturuldu + Aynı ada sahip başka bir İçerik Şablonu zaten var + İçerik Şablonu, bir düzenleyicinin yeni içerik oluşturmak için temel olarak kullanmak üzere seçebileceği önceden tanımlanmış içeriktir Yüklemek için tıklayın + veya dosyaları seçmek için burayı tıklayın + Dosyaları yüklemek için buraya sürükleyebilirsiniz + Bu dosya yüklenemiyor, onaylanmış bir dosya türüne sahip değil + Maksimum dosya boyutu + Medya kökü + Medya taşınamadı + Medya kopyalanamadı + %0% üst kimliği altında klasör oluşturulamadı + %0% kimliğine sahip klasör yeniden adlandırılamadı + Dosyalarınızı alana sürükleyip bırakın + + + Yeni üye oluştur + Tüm Üyeler + Üye gruplarının düzenlenecek ek özellikleri yoktur. - Nerede yeni %0% yaratmak istiyorsun + Yeni %0% 'i nerede oluşturmak istiyorsunuz Altında bir öğe oluşturun - Bir tür ve bir başlık seçin - "belge türleri".]]> - "ortam türleri".]]> + İçerik şablonu yapmak istediğiniz belge türünü seçin + Bir klasör adı girin + Bir tür ve başlık seçin + İzinler altında İzin verilen alt düğüm türlerini düzenleyerek Ayarlar bölümündeki Belge Türleri 'nde etkinleştirmelisiniz.]]> + Ayarlar bölümündeki Belge Türleri 'nde oluşturmanız gerekir.]]> + İçerik ağacında seçilen sayfa, altında herhangi bir sayfanın oluşturulmasına izin vermiyor. + Bu belge türü için izinleri düzenleyin + Yeni bir belge türü oluşturun + İzinler altında Kök olarak izin ver seçeneğini değiştirerek, Ayarlar bölümündeki Belge Türleri 'nde etkinleştirmeniz gerekir. ]]> + İzinler altında İzin verilen alt düğüm türlerini düzenleyerek Ayarlar bölümündeki Medya Türleri 'nde etkinleştirmelisiniz. .]]> + Ağaçtaki seçili ortam, altında başka bir ortamın oluşturulmasına izin vermiyor. + Bu medya türü için izinleri düzenleyin + Şablonsuz Belge Türü + Yeni klasör + Yeni veri türü + Yeni JavaScript dosyası + Yeni boş kısmi görünüm + Yeni kısmi görünüm makrosu + Ön bilgiden yeni kısmi görünüm + Snippet'ten yeni kısmi görünüm makrosu + Yeni kısmi görünüm makrosu (makrosuz) + Yeni stil sayfası dosyası + Yeni Zengin Metin Düzenleyicisi stil sayfası dosyası - 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 + Web sitenize göz atın + - Gizle + Umbraco açılmıyorsa, bu siteden pop-up'lara izin vermeniz gerekebilir + yeni bir pencerede açıldı + Yeniden Başlat + Ziyaret edin + Hoş geldiniz + + + Kal + Değişiklikleri sil + Kaydedilmemiş değişiklikleriniz var + Bu sayfadan ayrılmak istediğinizden emin misiniz? - kaydedilmemiş değişiklikleriniz var + Yayınlama, seçili öğelerin sitede görünür olmasını sağlar. + Yayından kaldırıldığında, seçili öğeler ve bunların tüm alt öğeleri siteden kaldırılır. + Yayından kaldırıldığında bu sayfa ve tüm soyundan gelenler siteden kaldırılır. + Kaydedilmemiş değişiklikleriniz var. Belge Türünde değişiklik yapmak değişiklikleri geçersiz kılacaktır. + + + Bitti + %0% öğe silindi + %0% öğe silindi + %1% öğeden %0% silindi + %1% öğeden %0% silindi + %0% öğe yayınlandı + %0% öğe yayınlandı + %1% öğeden %0% yayınlandı + %1% öğeden %0% yayınlandı + Yayınlanmamış %0% öğe + Yayınlanmamış %0% öğe + Yayınlanmamış%1% öğeden %0% + Yayınlanmamış%1% öğeden %0% + %0% öğe taşındı + %0% öğe taşındı + %0%,%1% öğeden taşındı + %1% öğeden %0% oranında taşındı + %0% öğe kopyalandı + %0% öğe kopyalandı + %1% öğeden %0% kopyalandı + %1% öğeden %0% kopyalandı - isim - konak yönetin - Bu pencereyi kapatın - Silmek istediğine emin misin - Eğer devre dışı bırakmak istediğinizden emin misiniz + Bağlantı başlığı + Bağlantı + Bağlayıcı / sorgu dizesi + Ad + Ana bilgisayar adlarını yönet + Bu pencereyi kapat + Silmek istediğinizden emin misiniz + %1% temelinde %0% 'ı silmek istediğinizden emin misiniz + Devre dışı bırakmak istediğinizden emin misiniz + Kaldırmak istediğinizden emin misiniz + %0% kullanımını kaldırmak istediğinizden emin misiniz?]]> + %0% başvurusunu kaldırmak istediğinizden emin misiniz?]]> Emin misiniz? Emin misiniz? Kes - Düzenleme Sözlük Öğe - Dil Düzenleme - Yerel bağlantı ekleme - Karakter Ekle - Grafik başlığı ekleyin + Sözlük Öğesini Düzenle + Dili Düzenle + Seçili medyayı düzenle + Yerel bağlantı ekle + Karakter ekle + Grafik başlığı ekle Resim ekle - Link Ekle - Bir makro eklemek için tıklayın - Tablo Ekle - Son DÜzenleme + Link ekle + Makro eklemek için tıklayın + Tablo ekle + Bu, dili silecek + Bir dil için kültürü değiştirmek pahalı bir işlem olabilir ve içerik önbelleğinin ve dizinlerin yeniden oluşturulmasına neden olabilir + Son Düzenleme Bağlantı - İç Bağlantı: - Yerel bağlantıları kullanırken, bağlantının önündeki "# " insert - Yeni pencerede aç - Düzenleyebileceğiniz Bu makro herhangi bir özellikleri içermiyor + Dahili bağlantı: + Yerel bağlantıları kullanırken, bağlantının önüne "#" ekleyin + Yeni pencerede açılsın mı? + Makro Ayarları + Bu makro düzenleyebileceğiniz herhangi bir özellik 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 + için izinleri düzenle + için izinleri ayarlayın + %1% kullanıcı grubu için %0% için izinleri ayarla + İzinlerini ayarlamak istediğiniz kullanıcı gruplarını seçin + Geri dönüşüm kutusundaki öğeler artık siliniyor. Lütfen bu işlem yapılırken bu pencereyi kapatmayın 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ı + Öğeler geri dönüşüm kutusundan silindiğinde sonsuza kadar kaybolacaklar + regexlib.com 'un web hizmeti şu anda bazı sorunlar yaşıyor ve biz üzerinde kontrol yok. Bu rahatsızlıktan dolayı çok üzgünüz.]]> + Bir form alanına doğrulama eklemek için normal ifade arayın. Örnek: 'e-posta,'posta kodu','URL' + Makroyu Kaldır + Gerekli Alan + Site yeniden dizine eklendi + Web sitesi önbelleği yenilendi. Tüm yayın içeriği artık güncel. Yayınlanmamış içeriğin tamamı hâlâ yayınlanmamış olsa da + Web sitesi önbelleği yenilenecek. Yayınlanan tüm içerik güncellenecek, yayınlanmamış içerik ise yayınlanmayacak. + Sütun sayısı Satır sayısı - Tam boyutta görmek için resmin üzerine tıklayın - öğeyi seçin - Görünüm Önbellek Öğe + Tam boyutta görmek için resmi tıklayın + Öğe seçin + Önbellek Öğesini Görüntüle + Orijinalle ilişkilendir + Torunları dahil et + En arkadaş canlısı topluluk + Sayfaya bağla + Bağlı belgeyi yeni bir pencerede veya sekmede açar + Medyaya bağlantı + İçerik başlangıç ​​düğümünü seçin + Medya seçin + Ortam türünü seçin + Simge seçin + Öğeyi seçin + Bağlantı seçin + Makro seçin + İçerik seçin + İçerik türünü seçin + Medya başlangıç ​​düğümünü seçin + Üye seç + Üye grubu seçin + Üye türünü seçin + Düğüm seçin + Bölümleri seçin + Kullanıcı seçin + Kullanıcıları seçin + ​​Simge bulunamadı + Bu makro için parametre yok + Eklenecek makro yok + Harici giriş sağlayıcıları + İstisna Ayrıntıları + Yığın İzleme + İç İstisna + Bağlayın + Bağlantınızı kaldırın + hesap + Düzenleyici seçin + Snippet seçin + Bu, düğümü ve tüm dillerini silecektir. Yalnızca bir dili silmek istiyorsanız, bunun yerine düğümü o dilde yayından kaldırmalısınız. + %0% kullanıcısını kaldıracaktır.]]> + %0% kullanıcısını %1% grubundan kaldıracak]]> + Evet, kaldır + + + Sözlük öğesi yok. - %0%
' aşağıda
Sol taraftaki menüden 'diller' başlığı altında ek dil ekleyebilirsiniz - ]]> - Kültür adı + + %0%' için farklı dil sürümlerini düzenleyin + ]]> + + Kültür Adı + + + + Sözlüğe genel bakış + + + Yapılandırılmış Arayıcılar + Yapılandırılmış herhangi bir Searcher için özellikleri ve araçları gösterir (yani, çoklu dizin arayıcı gibi) + Alan değerleri + Sağlık durumu + Dizinin sağlık durumu ve okunabiliyorsa + Dizin oluşturucular + Dizin bilgisi + Dizinin özelliklerini listeler + İnceleme dizinlerini yönetin + Her dizinin ayrıntılarını görüntülemenizi sağlar ve dizinleri yönetmek için bazı araçlar sağlar + Dizini yeniden oluştur + + + Sitenizde ne kadar içerik olduğuna bağlı olarak bu biraz zaman alabilir.
+ Yüksek web sitesi trafiğinin olduğu zamanlarda veya editörler içeriği düzenlerken bir dizinin yeniden oluşturulması önerilmez. + ]]> +
+ Arayanlar + Dizini arayın ve sonuçları görüntüleyin + Araçlar + Dizini yönetmek için araçlar + alanlar + Dizin okunamıyor ve yeniden oluşturulması gerekecek + İşlem beklenenden uzun sürüyor, bu işlem sırasında herhangi bir hata olup olmadığını görmek için Umbraco günlüğünü kontrol edin + Bu dizin, atanmış olmadığı için yeniden oluşturulamaz + IIndexPopulator - 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 ... + Kullanıcı adınızı girin + Şifrenizi girin + Şifrenizi onaylayın + %0% olarak adlandırın ... + Bir ad girin ... + Bir e-posta girin ... + Bir kullanıcı adı girin ... + Etiket ... + Bir açıklama girin ... + Aramak için yazın ... + Filtrelemek için yazın ... + Etiket eklemek için yazın (her etiketten sonra enter tuşuna basın) ... + E-postanızı girin + Bir mesaj girin ... + Kullanıcı adınız genellikle e-postanızdır + # değer veya? anahtar=değer + Takma ad girin ... + Takma ad oluşturuluyor ... + Öğe oluştur + Oluştur + Düzenle + Ad - Özel liste görünüm oluşturun - Özel liste görünümü kaldır + Özel liste görünümü oluştur + Özel liste görünümünü kaldır + Bu takma ada sahip bir içerik türü, medya türü veya üye türü zaten var + + + Yeniden adlandırıldı + Buraya yeni bir klasör adı girin + %0%,%1% olarak yeniden adlandırıldı Ön değer ekle - Veritabanı veritürü - Mülkiyet editörü GUID - Mülkiyet editörü + Veritabanı veri türü + Özellik düzenleyici GUID + Mülk düzenleyici Düğmeler - Gelişmiş ayarları etkinleştir... + için gelişmiş ayarları etkinleştir Bağlam menüsünü etkinleştir - Eklenen görüntülerin maksimum varsayılan boyutu - İlgili stil + Eklenen resimlerin maksimum varsayılan boyutu + İlgili stil sayfaları Etiketi göster - Yükseklik ve Genişlik + Genişlik ve yükseklik + Tüm mülk türleri & emlak verileri + Bu veri türünü kullanan kalıcı olarak silinecek, lütfen bunları da silmek istediğinizi onaylayın + Evet, sil + ve tüm mülk türleri & bu veri türünü kullanan mülk verileri + Taşınacak klasörü seçin + aşağıdaki ağaç yapısına + altına taşındı - 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 + Verileriniz kaydedildi, ancak bu sayfayı yayınlamadan önce düzeltmeniz gereken bazı hatalar var: + Mevcut üyelik sağlayıcısı, şifre değiştirmeyi desteklemiyor (EnablePasswordRetrieval'in doğru olması gerekiyor) + %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 + Parola minimum %0% karakter uzunluğunda olmalı ve en az%1% alfa olmayan sayısal karakter (ler) içermelidir + %0% tam sayı olmalıdır + %1% sekmesindeki %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 + %1% konumunda %0% doğru biçimde değil + %0% 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%) + Sunucudan bir hata aldı + Belirtilen dosya türüne yönetici tarafından izin verilmedi + NOT! CodeMirror yapılandırma ile etkinleştirilmiş olsa da, yeterince kararlı olmadığı için Internet Explorer'da devre dışı bırakılmıştır. + Lütfen yeni özellik türünde hem takma adı hem de adı girin! + Belirli bir dosya veya klasöre okuma/yazma erişiminde sorun var + Kısmi Görünüm komut dosyası yüklenirken hata oluştu (dosya: %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? - 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. + Resmi orijinal boyutundan daha büyük yapmak üzeresiniz. Devam etmek istediğinizden emin misiniz? + Başlangıç ​​düğümü silindi, lütfen yöneticinizle iletişime geçin + Lütfen stili değiştirmeden önce içeriği işaretleyin + Etkin stil yok + Lütfen imleci birleştirmek istediğiniz iki hücrenin soluna yerleştirin + Birleştirilmemiş bir hücreyi bölemezsiniz. + Bu özellik geçersiz Hakkında - Eylem - Eylemler + İşlem + İşlemler Ekle - Takma ad + Takma reklam + Tümü Emin misiniz? - Sınır - tarafında + Geri + Genel bakışa dön + Kenarlık + yazan İptal - hücre marjı - Seçim + Hücre kenar boşluğu + Seçin + Temizle Kapat - Pencereyi kapat - Açıklama + Pencereyi Kapat + Yorum Onayla - oranları sınırlamak + Sınırla + Oranları sınırlayın + İçerik Devam et Kopyala Oluştur Veritabanı Tarih - Standart + Varsayılan Sil Silindi - Siliniyor... - Dizayn + Siliniyor ... + Tasarım + Sözlük Boyutlar + Sil Aşağı İndir Düzenle Düzenlendi - Elemanları - E-Posta + Öğeler + E-posta Hata + Alan Bul + İlk + Odak noktası + Genel + Gruplar + Grup Yükseklik Yardım - İkon - İthalat - İç Marj + Gizle + Geçmiş + Simge + Kimlik + İçe Aktar + Alt klasörleri aramaya dahil et + Yalnızca bu klasörü ara + Bilgi + İç kenar boşluğu Ekle - Kur - Satır Uzunluğu + Yükle + Geçersiz + Yasla + Etiket Dil + Son Düzen + Bağlantılar Yükleniyor Kilitli - Giriş yap - Oturum Kapat - Çıkış yap + Giriş + Oturumu kapat + Çıkış Makro + Zorunlu + Mesaj Taşı Ad Yeni Sonraki Hayır arasında - TAMAM + Kapalı + Tamam + Seçenekler + Açık veya - Parola + Sıralama ölçütü + Şifre Yol - Bir dakika lütfen... + Bir dakika lütfen ... Önceki Özellikler + Yeniden Oluştur Form verilerini almak için e-posta - Geridönüşüm kutusu + Geri Dönüşüm Kutusu + Geri dönüşüm kutunuz boş + Yeniden yükle Kalan - Adını Değiştir + Kaldır + Yeniden adlandır Yenile Gerekli - Tekrar dene - İzinler - Arama + Al + Yeniden dene + daha + Planlanmış Yayınlama + Ara + Üzgünüz, aradığınızı bulamıyoruz. + Hiçbir öğe eklenmedi Sunucu + Ayarlar Göster - Gönder sayfasını göster + Sayfayı Gönderildiğinde Göster Boyut Sırala - Tip - Aramak için yazın... + Durum + Gönder + Başarılı + Tür + Aramak için yazın ... + altında Yukarı Güncelle Yükselt @@ -372,18 +762,60 @@ Kullanıcı Kullanıcı adı Değer - Görünüm - Hoşgeldiniz... + Görüntüle + Hoş geldiniz ... Genişlik Evet Klasör - Arama Sonuçları + Arama sonuçları + Yeniden sırala + Yeniden sıralamayı tamamladım + Önizleme + Şifreyi değiştir + için + Liste görünümü + Kaydediliyor ... + mevcut + Göm + seçildi + Diğer + Makaleler + Videolar + Temizle + Kuruluyor + + + Mavi + + + Grup ekle + Mülk ekle + Düzenleyici ekle + Şablon ekle + Alt düğüm ekle + Çocuk ekle + Veri türünü düzenle + Bölümlere git + Kısayollar + kısayolları göster + Liste görünümünü değiştir + Kök olarak izin ver arasında geçiş yap + Yorum / Yorum kaldırma satırları + Satırı kaldır + Satırları Yukarı Kopyala + Satırları Aşağı Kopyala + Satırları Yukarı Taşı + Satırları Aşağı Taşı + Genel + Düzenleyici + Kültür varyantlarına izin ver + Bölümlemeye izin vermeyi aç/kapat Arka plan rengi Kalın - Metin Rengi - Yazı + Metin rengi + Yazı Tipi Metin @@ -391,570 +823,1755 @@ 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.... + web.config dosyası kaydedilemedi. Lütfen bağlantı dizesini manuel olarak değiştirin. + ​​Veritabanınız bulundu ve tanımlandı 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.

+ + kur düğmesine basın + ]]> + + İleri'ye basın.]]> + + ​​ Veritabanı bulunamadı! Lütfen "web.config" dosyasının "bağlantı dizesindeki" bilgilerin doğru olup olmadığını kontrol edin.

+

Devam etmek için lütfen "web.config" dosyasını düzenleyin (Visual Studio veya favori metin düzenleyicinizi kullanarak), en alta kaydırın, veritabanınız için "UmbracoDbDSN" adlı anahtara bağlantı dizesini ekleyin 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.]]> - Yeniden dene düğmesini tıklayın. + bitti.
+ Web.config'i düzenleme hakkında daha fazla bilgi burada.

]]> +
+ + + Lütfen gerekirse ISS'niz ile iletişime geçin. + Yerel bir makineye veya sunucuya kurulum yapıyorsanız, sistem yöneticinizden bilgi almanız gerekebilir.]]> + + + - CMS %0% için veritabanını yükseltme için yükseltme düğmesine basın -

+ Veritabanınızı Umbraco %0% sürümüne yükseltmek için yükselt düğmesine basın

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

- ]]>
- 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 - + + + için İleri 'ye basın + ilerlemek. ]]> + + ileri 'yi tıklayın]]> + Varsayılan kullanıcıların şifresinin değiştirilmesi gerekiyor! ]]> + Varsayılan kullanıcı devre dışı bırakıldı veya Umbraco'ya erişimi yok!

Başka işlem yapılmasına gerek yok. Devam etmek için İleri 'yi tıklayın.]]> + Varsayılan kullanıcının şifresi kurulumdan bu yana başarıyla değiştirildi!

Başka işlem yapılmasına gerek yok. Devam etmek için İleri 'yi tıklayın.]]> + Şifre değiştirildi! + Harika bir başlangıç ​​yapın, tanıtım videolarımızı izleyin + Sonraki düğmeye tıklayarak (veya web.config'deki umbracoConfigurationStatus'u değiştirerek), bu yazılımın lisansını aşağıdaki kutuda belirtildiği şekilde kabul etmiş olursunuz. Bu Umbraco dağıtımının iki farklı lisanstan oluştuğuna dikkat edin, çerçeve için açık kaynak MIT lisansı ve kullanıcı arayüzünü kapsayan ücretsiz Umbraco lisansı. + Henüz yüklenmedi. + Etkilenen dosyalar ve klasörler + Umbraco için izinlerin ayarlanmasıyla ilgili daha fazla bilgiyi burada bulabilirsiniz + Aşağıdaki dosyalara / klasörlere ASP.NET değiştirme izinleri vermeniz gerekiyor + + İzin ayarlarınız neredeyse mükemmel!

+ Umbraco'yu sorunsuz bir şekilde çalıştırabilirsiniz, ancak Umbraco'dan tam olarak yararlanmanız için önerilen paketleri kuramazsınız.]]> +
+ Nasıl Çözümlenir + Metin sürümünü okumak için burayı tıklayın + eğitim videosunu izleyin veya metin sürümünü okuyun.]]> + + İzin ayarlarınız bir sorun olabilir! +

+ Umbraco'yu sorunsuz bir şekilde çalıştırabilirsiniz, ancak Umbraco'dan tam olarak yararlanmanız için önerilen klasörler oluşturamaz veya paketleri yükleyemezsiniz.]]> +
+ + İzin ayarlarınız Umbraco için hazır değil! +

+ Umbraco'yu çalıştırmak için izin ayarlarınızı güncellemeniz gerekir.]]> +
+ + İzin ayarlarınız mükemmel!

+ Umbraco'yu çalıştırmaya ve paketleri kurmaya hazırsınız!]]> +
+ Klasör sorununu çözme + ASP.NET ile ilgili sorunlar ve klasör oluşturma hakkında daha fazla bilgi için bu bağlantıyı izleyin + Klasör izinlerini ayarlama + + + + Sıfırdan başlamak istiyorum + + nasıl yapılacağını öğrenin ) + Yine de Runway'i daha sonra kurmayı seçebilirsiniz. Lütfen Geliştirici bölümüne gidin ve Paketleri seçin. + ]]> + + Temiz bir Umbraco platformu kurdunuz. Bundan sonra ne yapmak istiyorsunuz? + Runway yüklendi + + + Bu, önerilen modüller listemizdir, yüklemek istediklerinizi işaretleyin veya modüllerin tam listesi + ]]> + + Yalnızca deneyimli kullanıcılar için önerilir + Basit bir web sitesiyle başlamak istiyorum + + - "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. + "Runway", bazı temel belge türlerini ve şablonlarını sunan basit bir web sitesidir. Yükleyici sizin için otomatik olarak Runway kurabilir, + ancak kolayca düzenleyebilir, genişletebilir veya kaldırabilirsiniz. Bu gerekli değildir ve Umbraco'yu onsuz mükemmel bir şekilde kullanabilirsiniz. Ancak, + Runway, her zamankinden daha hızlı başlamanız için en iyi uygulamalara dayalı kolay bir temel sunar. + Runway'i kurmayı seçerseniz, Runway sayfalarınızı geliştirmek için isteğe bağlı olarak Pist Modülleri adı verilen temel yapı bloklarını seçebilirsiniz.

- Included with Runway: Home page, Getting Started page, Installing Modules page.
- Optional Modules: Top Navigation, Sitemap, Contact, Gallery. + Runway'e Dahildir: Ana sayfa, Başlarken sayfası, Modülleri Takma sayfası.
+ İsteğe Bağlı Modüller: Üst Gezinme, Site Haritası, İletişim, Galeri.
- ]]>
- 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.]]>
+ ]]> +
+ Runway Nedir + Adım 1/5 Lisansı kabul edin + Adım 2/5: Veritabanı yapılandırması + Adım 3/5: Dosya İzinlerini Doğrulama + 4. Adım: Umbraco güvenliğini kontrol edin + Adım 5/5: Umbraco başlamanıza hazır + Umbraco'yu seçtiğiniz için teşekkür ederiz + + Yeni sitenize göz atın +Runway'i kurdunuz, öyleyse neden yeni web sitenizin nasıl göründüğüne bakmıyorsunuz.]]> + + + Daha fazla yardım ve bilgi +Ödüllü topluluğumuzdan yardım alın, belgelere göz atın veya basit bir sitenin nasıl oluşturulacağı, paketlerin nasıl kullanılacağı ve Umbraco terminolojisine yönelik hızlı bir kılavuzla ilgili bazı ücretsiz videolar izleyin]]> + + Umbraco %0% yüklendi ve kullanıma hazır + + /web.config dosyasını manuel olarak düzenleyin ve alttaki AppSetting anahtarını UmbracoConfigurationStatus '%0%' değerine güncelleyin.]]> + + + anında başlayabilirsiniz .
Umbraco'da yeniyseniz , +başlangıç ​​sayfalarımızda birçok kaynak bulabilirsiniz.]]> +
+ + Umbraco’yu Başlat +Web sitenizi yönetmek için, Umbraco'nun arka ofisini açın ve içerik eklemeye başlayın, şablonları ve stil sayfalarını güncelleyin veya yeni işlevler ekleyin]]> + + Veritabanına bağlantı başarısız oldu. + Umbraco Sürüm 3 + Umbraco Sürüm 4 + İzle + + Umbraco %0% 'u yapılandırma sürecinde size yol gösterecektir. +

+ Sihirbazı başlatmak için "ileri" seçeneğine basın.]]> +
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 + Boştaydınız ve çıkış otomatik olarak içinde gerçekleşecek + Çalışmanızı kaydetmek için şimdi yenileyin - Pazar - Pazartesi - Salı - Çarşamba - Perşembe - Cuma - İçerik Yönetim Sistemi - Giriş Yapın + Mutlu Pazarlar + Mutlu manik Pazartesi + Mutlu salı günleri + Harika Çarşamba Günleri + Mutlu, gök gürültülü Perşembe + Mutlu Cuma Günü + Mutlu Yıllar + Aşağıda oturum açın + ile oturum açın Oturum zaman aşımına uğradı - © 2015 - %0%
umbraco.com

]]>
+ © 2015 - %0%
Umbraco.com

]]>
+ Şifrenizi mi unuttunuz? + Parolanızı sıfırlamak için bir bağlantıyla belirtilen adrese bir e-posta gönderilecektir + Kayıtlarımızla eşleşirse, şifre sıfırlama talimatlarını içeren bir e-posta, belirtilen adrese gönderilecektir + Şifreyi göster + Şifreyi gizle + Giriş formuna dön + Lütfen yeni bir şifre girin + Şifreniz güncellendi + Tıkladığınız bağlantı geçersiz veya süresi dolmuş + Umbraco: Şifreyi Sıfırla + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Şifre sıfırlama istendi +

+

+ Umbraco arka ofisinde oturum açmak için kullanıcı adınız: %0% +

+

+ + + + + + +
+ + Şifrenizi sıfırlamak için bu bağlantıyı tıklayın + +
+

+

Bağlantıya tıklayamazsanız, bu URL'yi kopyalayıp tarayıcı pencerenize yapıştırın:

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

+
+
+


+
+
+ + + ]]> +
- Gösterge Paneli + Dashboard 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 + Yukarıdaki sayfayı seçin ... + %0%,%1% konumuna kopyalandı + %0% belgesinin aşağıya kopyalanacağı yeri seçin + %0%,%1% konumuna taşındı + %0% belgesinin aşağıya taşınacağı yeri seçin + , yeni içeriğinizin kökü olarak seçildi, aşağıdaki 'tamam'ı tıklayın. + Henüz düğüm seçilmedi, lütfen 'tamam'ı tıklamadan önce yukarıdaki listeden bir düğüm seçin + Geçerli düğüme, türü nedeniyle seçilen düğüm altında izin verilmiyor + Geçerli düğüm, alt sayfalarından birine taşınamaz + Geçerli düğüm kökte bulunamaz + 1 veya daha fazla alt belge üzerinde yetersiz izniniz olduğundan işleme izin verilmiyor. + Kopyalanan öğeleri orijinalle ilişkilendir - 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% + %0%]]> için bildiriminizi seçin + için bildirim ayarları kaydedildi + + + + Aşağıdaki diller %0% değiştirildi + + + + + + + + +
+ + +
+ + + + + +
+ +
+ +
+
-

- - - -

Have a nice day!

- Cheers from the Umbraco robot -

]]>
- [%0%] Notification about %1% performed on %2% - Notifications + + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Merhaba %0%, +

+

+ Bu, '%1%' görevinin '%2%' sayfasında '%3%' kullanıcısı tarafından gerçekleştirildiğini size bildirmek için otomatik bir postadır. +

+ + + + + + +
+ +
+ DÜZENLE
+
+

+

Güncelleme Özeti:

+ %6% +

+

+ İyi günler dilerim!

+ Umbraco robotundan teşekkürler +

+
+
+


+
+
+ + + ]]> + + + Aşağıdaki diller değiştirildi:

+ %0% + ]]> +
+ [ %0%]%1% ile ilgili bildirim%2% üzerinde gerçekleştirildi + Bildirimler - - button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> - Author - 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.]]>
- 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.]]> - Package version + İşlemler + Oluşturuldu + Paket oluştur + + + Umbraco paketleri genellikle ".umb" veya ".zip" uzantısına sahiptir. + ]]> + + Bu, paketi silecek + Yüklemek için bırakın + Tüm alt düğümleri dahil et + veya paket dosyasını seçmek için burayı tıklayın + Paketi yükle + Makinenizden seçerek yerel bir paket kurun. Yalnızca bildiğiniz ve güvendiğiniz kaynaklardan paket yükleyin + Başka bir paket yükleyin + İptal edin ve başka bir paketi yükleyin + Kabul ediyorum + kullanım şartları + + Dosyaya giden yol + Dosyaya giden mutlak yol (yani: /bin/umbraco.bin) + Yüklendi + Yüklü paketler + Yerel yükle + Bitir + Bu pakette yapılandırma görünümü yok + Henüz paket oluşturulmadı + Kurulu paketiniz yok + 'Paketler' simgesini kullanarak mevcut paketlere göz atın]]> + Paket İşlemleri + Yazar URL'si + Paket İçeriği + Paket Dosyaları + Simge URL'si + Paketi yükle + Lisans + Lisans URL'si + Paket Özellikleri + Paket arayın + Sonuçlar + için hiçbir şey bulamadık + Lütfen başka bir paket aramayı deneyin veya kategorilere göz atın + Popüler + Yeni sürümler + vardır + karma noktaları + Bilgi + Sahip + Katkıda bulunanlar + Oluşturuldu + Mevcut sürüm + . NET sürümü + İndirmeler + Beğeniler + Uyumluluk + Bu paket, topluluk üyeleri tarafından bildirildiği üzere aşağıdaki Umbraco sürümleriyle uyumludur. % 100'ün altında rapor edilen sürümler için tam uyumluluk garanti edilemez + Harici kaynaklar + Yazar + Belgeler + Meta verilerini paketle + Paket adı + Paket herhangi bir öğe içermiyor + +
+ Aşağıdaki "Paketi kaldır"'ı tıklayarak bunu sistemden güvenle kaldırabilirsiniz.]]> +
+ Paket seçenekleri + Paketi beni oku + Paket deposu + Paketi kaldırmayı onayla + Paket kaldırıldı + Paket başarıyla kaldırıldı + Paketi kaldır + + + Uyarı: kaldırdığınız öğelere bağlı olarak herhangi bir belge, ortam vb. çalışmayı durdurur ve sistem kararsızlığına neden olabilir, + bu yüzden dikkatli bir şekilde kaldırın. Şüpheniz varsa, paket yazarıyla iletişime geçin.]]> + + Paket versiyonu + Paket zaten yüklü + Bu paket kurulamaz, minimum Umbraco sürümü gerektirir + Kaldırılıyor ... + İndiriliyor ... + İçe Aktarılıyor ... + Kuruluyor ... + Yeniden başlatılıyor, lütfen bekleyin ... + Her şey tamam, tarayıcınız şimdi yenilenecek, lütfen bekleyin ... + Kurulumu tamamlamak ve sayfayı yeniden yüklemek için lütfen 'Bitir'i tıklayın. + Paket yükleniyor ... - 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) + Tam biçimlendirmeyle yapıştırın (Önerilmez) + Yapıştırmaya çalıştığınız metin özel karakterler veya biçimlendirme içeriyor. Bunun nedeni Microsoft Word'den metin kopyalanması olabilir. Umbraco, özel karakterleri veya biçimlendirmeyi otomatik olarak kaldırabilir, böylece yapıştırılan içerik web için daha uygun olacaktır. + Hiçbir biçimlendirme olmadan ham metin olarak yapıştırın + Yapıştır, ancak biçimlendirmeyi kaldırın (Önerilir) - Role based protection - using Umbraco's member groups.]]> - 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 + Grup tabanlı koruma + Belirli üye gruplarının tüm üyelerine erişim vermek istiyorsanız + Grup tabanlı kimlik doğrulamasını kullanmadan önce bir üye grubu oluşturmanız gerekir + Hata Sayfası + Kişiler oturum açtığında ancak erişimi olmadığında kullanılır + %0%]]> + %0% artık korumalı]]> + %0%]]> sayfasından kaldırılmıştır. + Giriş Sayfası + Giriş formunu içeren sayfayı seçin + Korumayı kaldır ... + %0% sayfasından kaldırmak istediğinizden emin misiniz?]]> + Giriş formu ve hata mesajları içeren sayfaları seçin + %0% sayfasına erişimi olan grupları seçin]]> + %0% sayfasına erişimi olan üyeleri seçin]]> + Belirli üyelerin korunması + Belirli üyelere erişim vermek istiyorsanız - - - - - 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. - ]]>
+ + + + + + + + + + + + + + + + + Yayınlanmamış alt sayfaları dahil et + Yayınlanıyor - lütfen bekleyin ... + %1% sayfadan %0% yayınlandı ... + %0% yayınlandı + %0% ve alt sayfalar yayınlandı + %0% ve tüm alt sayfalarını yayınlayın + + %0% yayınlamak ve böylece içeriğini herkese açık hale getirmek için Yayınla'yı tıklayın.

+ Aşağıdaki Yayınlanmamış alt sayfaları ekle 'yi işaretleyerek bu sayfayı ve tüm alt sayfalarını yayınlayabilirsiniz. + ]]> +
- You have not configured any approved colours + Onaylanmış herhangi bir renk yapılandırmadınız + + + Yalnızca şu türdeki öğeleri seçebilirsiniz: %0% + Şu anda silinmiş veya geri dönüşüm kutusunda bulunan bir içerik öğesini seçtiniz + Şu anda silinmiş veya geri dönüşüm kutusunda bulunan içerik öğelerini seçtiniz + + + Silinen öğe + Şu anda silinmiş veya geri dönüşüm kutusunda bulunan bir medya öğesini seçtiniz + Şu anda silinmiş veya geri dönüşüm kutusunda bulunan medya öğelerini seçtiniz + Çöp kutusuna gönderildi - harici bağlantı gir - iç sayfa seç - Altyazı + harici bağlantı girin + dahili sayfayı seç + Başlık Bağlantı - yeni pencere - Yeni altyazı gir - Bağlantı gir + Yeni pencerede aç + ekran başlığını girin + Bağlantıyı girin - Sıfırla + Kırpmayı sıfırla + Kırpmayı kaydet + Yeni kırpma ekle + Bitti + Düzenlemeleri geri alın + Kullanıcı tanımlı - 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 + Değişiklikler + Oluşturuldu + Mevcut sürüm + Kırmızı metin seçili sürümde gösterilmeyecektir. , yeşil eklendi demektir ]]> + Belge geri alındı ​​ + Mevcut sürümle karşılaştırmak için bir sürüm seçin + Bu, seçilen sürümü HTML olarak görüntüler, 2 sürüm arasındaki farkı aynı anda görmek isterseniz, fark görünümünü kullanın + Geri alın + Sürüm seçin + Görüntüle - Düzenleme komut dosyası + Komut dosyasını düzenle - Kapıcı + Konsiyerj İçerik - Kurya + Kurye Geliştirici - CMS Yapılandırma Sihirbazı + Formlar + Yardım + Umbraco Yapılandırma Sihirbazı Medya Üyeler - Haber Bültenleri + Bültenler + Paketler Ayarlar - İstatistik + İstatistikler Çeviri Kullanıcılar - Yardım - Formlar - Kayadata + Turlar + En iyi Umbraco video eğitimleri + our.umbraco.com adresini ziyaret edin + umbraco.tv'yi ziyaret edin - Varsayılan şablonu - 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 - 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. - + Varsayılan şablon + + Bir belge türünü içe aktarmak için, "Gözat" düğmesini tıklayarak bilgisayarınızda ".udt" dosyasını bulun ve "İçe Aktar" ı tıklayın (sonraki ekranda onay vermeniz istenir) + Yeni Sekme Başlığı + Düğüm türü + Tür + Stil Sayfası + Komut Dosyası + Sekme + Sekme Başlığı + Sekmeler + Ana İçerik Türü etkinleştirildi + Bu İçerik Türü kullanır + Bu sekmede tanımlanmış özellik yok. Yeni bir mülk oluşturmak için üstteki "yeni mülk ekle" bağlantısını tıklayın. + Eşleşen şablon oluştur + Simge ekle + - 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 - + Sıralama düzeni + Oluşturma tarihi + Sıralama tamamlandı. + Nasıl düzenleneceklerini ayarlamak için farklı öğeleri aşağı veya yukarı sürükleyin. Veya tüm öğe koleksiyonunu sıralamak için sütun başlıklarını tıklayın + - Hata - Kullanıcı izniniz yeterli olmadığı için, işleminiz gerçekleştirilmedi. + Doğrulama + Öğe kaydedilmeden önce doğrulama hataları düzeltilmelidir + Başarısız + Kaydedildi + Yetersiz kullanıcı izni, işlemi tamamlayamadı İ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. + İşlem, üçüncü taraf bir eklenti tarafından iptal edildi + Yayınlama, üçüncü taraf bir eklenti tarafından iptal edildi + Mülk türü zaten var + Mülk türü oluşturuldu + DataType:%1%]]> + Mülk türü silindi + Belge Türü kaydedildi + Sekme oluşturuldu + Sekme silindi + Şu kimliğe sahip sekme: %0% silindi + Stil sayfası kaydedilmedi + Stil sayfası kaydedildi + Stil sayfası hatasız kaydedildi + Veri türü kaydedildi + Sözlük öğesi kaydedildi + Üst sayfa yayınlanmadığı için yayınlama 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 + ve web sitesinde görünür + İçerik kaydedildi + Değişiklikleri görünür kılmak için yayınlamayı unutmayın + Onay İçin Gönderildi + Değişiklikler onay için gönderildi Medya kaydedildi - Medya sorunsuz kaydedildi + Üye grubu kaydedildi + Medya hatasız 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 + Stil Sayfası Özelliği Kaydedildi + Stil Sayfası kaydedildi + Şablon kaydedildi + Kullanıcı kaydedilirken hata oluştu (günlüğü kontrol edin) + Kullanıcı Kaydedildi + Kullanıcı türü kaydedildi + Kullanıcı grubu kaydedildi + Kaydedilen kültürler ve ana bilgisayar adları + Kültürleri ve ana bilgisayar adlarını kaydetme hatası + Dosya kaydedilmedi + dosyası kaydedilemedi. Lütfen dosya izinlerini kontrol edin Dosya kaydedildi - Dosya sorunsuz kaydedildi + Dosya hatasız kaydedildi Dil kaydedildi - Template kaydedilmedi - Aynı isim ile 2 template bulunmadığından emin olun - Template kaydedildi - Template sorunsuz kaydedildi! - İçerik yayından kaldırıldı - Partial view kaydedildi - Partial view sorunsuz kaydedildi! - Partial view kaydedilmedi - Dosya kaydedilirken bir hata oluştu. + Medya Türü kaydedildi + Üye Türü kaydedildi + Üye Grubu kaydedildi + Şablon kaydedilmedi + Lütfen aynı takma ada sahip 2 şablonunuz olmadığından emin olun + Şablon kaydedildi + Şablon hatasız kaydedildi! + Yayınlanmamış içerik + Kısmi görünüm kaydedildi + Kısmi görünüm, herhangi bir hata olmadan kaydedildi! + Kısmi görünüm kaydedilmedi + Dosyayı kaydederken bir hata oluştu. + için kaydedilen izinler + %0% kullanıcı grubu silindi + %0% silindi + %0% kullanıcı etkinleştirildi + %0% kullanıcı devre dışı bırakıldı + %0% artık etkinleştirildi + %0% artık devre dışı + Kullanıcı grupları ayarlandı + %0% kullanıcının kilidi kaldırıldı + %0% artık kilitli + Üye dosyaya aktarıldı + Üyeyi dışa aktarırken bir hata oluştu + Kullanıcı %0% silindi + Kullanıcıyı davet et + Davet %0% 'a yeniden gönderildi + Belge türü dosyaya aktarıldı + Belge türü dışa aktarılırken 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 + Stil ekle + Stili düzenle + Zengin metin düzenleyici stilleri + Bu stil sayfası için zengin metin düzenleyicide bulunması gereken stilleri tanımlayın + Stil sayfasını düzenle + Stil sayfası özelliğini düzenle + Düzenleyici stili seçicide görüntülenen ad + Önizleme + Zengin metin düzenleyicide metin nasıl görünecek. + Seçici + CSS sözdizimini kullanır, ör. "h1" veya ".redHeader" Stiller + Zengin metin düzenleyicide uygulanması gereken CSS, ör. "color: red;" + Kod + Düzenleyici - 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 + %0% kimliğine sahip şablon silinemedi + Şablonu düzenle + Bölümler + İçerik alanı ekle + İçerik alanı yer tutucusu ekle + Ekle + Şablonunuza ne ekleyeceğinizi seçin + Sözlük öğesi + Sözlük öğesi, çevrilebilir bir metin parçası için yer tutucudur ve çok dilli web siteleri için tasarımlar oluşturmayı kolaylaştırır. + Makro + + Makro, yapılandırılabilir bir bileşendir ve aşağıdakiler için idealdir: + parametreler sağlama seçeneğine ihtiyaç duyduğunuz tasarımınızın yeniden kullanılabilir parçaları, + galeriler, formlar ve listeler gibi. + + Değer + Geçerli sayfadaki adlandırılmış bir alanın değerini, değeri değiştirme veya alternatif değerlere geri dönüş seçenekleri ile birlikte görüntüler. + Kısmi görünüm + + Kısmi görünüm, başka bir şablonun içinde oluşturulabilen ayrı bir şablon dosyasıdır. + şablonu, işaretlemeyi yeniden kullanmak veya karmaşık şablonları ayrı dosyalara ayırmak için harikadır. + + Ana şablon + Ana yok + Alt şablonu oluştur + + @RenderBody() yer tutucusu. + ]]> + + Adlandırılmış bir bölüm tanımlayın + + @section { ... }. Bu bir + @RenderSection kullanarak bu şablonun üst öğesinin belirli bir alanı. + ]]> + + Adlandırılmış bir bölüm oluşturun + + @RenderSection(ad) yer tutucusu ekleyerek bir alt şablonun adlandırılmış alanını oluşturur. + Bu, karşılık gelen bir @section [ad] {...} tanımına sarılmış bir alt şablon alanını oluşturur. + ]]> + + Bölüm Adı + Bölüm zorunludur + @section tanımı içermelidir, aksi takdirde bir hata gösterilir. + ]]> + Sorgu oluşturucu + öğe iade edildi, + panoya kopyala + istiyorum + tüm içerik + "%0%" türünde içerik + dan + web sitem + nerede + ve + = + değil + önce + önce (seçilen tarih dahil) + sonra + sonra (seçilen tarih dahil) + şuna eşittir + eşit değildir + içerir + içermez + büyüktür + büyük veya eşittir + küçüktür + küçüktür veya eşittir + kimlik + Ad + Oluşturulma Tarihi + Son Güncelleme Tarihi + sırala + artan + azalan + Şablon - Image - Macro - 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 - Allow all editors - Allow all row configurations + Resim + Makro + İçerik türünü seçin + Bir düzen seçin + Satır ekle + İçerik ekle + İçeriği bırak + Ayarlar uygulandı + Bu içeriğe burada izin verilmiyor + Bu içeriğe burada izin verilir + Yerleştirmek için tıklayın + Resim eklemek için tıklayın + Resim yazısı ... + Buraya yazın ... + Izgara Düzenleri + Düzenler, ızgara düzenleyicinin genel çalışma alanıdır, genellikle yalnızca bir veya iki farklı düzene ihtiyacınız vardır + Izgara Düzeni Ekle + Izgara Düzenini Düzenle + Sütun genişliklerini ayarlayarak ve ek bölümler ekleyerek düzeni ayarlayın + Satır yapılandırmaları + Satırlar, yatay olarak düzenlenmiş önceden tanımlanmış hücrelerdir + Satır yapılandırması ekle + Satır yapılandırmasını düzenle + Hücre genişliklerini ayarlayarak ve ilave hücreler ekleyerek satırı ayarlayın + Sütunlar + Izgara düzenindeki toplam birleşik sütun sayısı + Ayarlar + Hangi ayarları düzenleyicilerin değiştirebileceğini yapılandırın + Stiller + Stil editörlerinin neleri değiştirebileceğini yapılandırma + Tüm düzenleyicilere izin ver + Tüm satır yapılandırmalarına izin ver + Maksimum öğe + Boş bırakın veya sınırsız için 0 olarak ayarlayın + Varsayılan olarak ayarla + Fazladan birini seçin + Varsayılanı seçin + eklendi + Uyarı + Satır yapılandırmasını siliyorsunuz + + Bir satır yapılandırma adının silinmesi, bu yapılandırmaya dayalı mevcut herhangi bir içerik için veri kaybına neden olur. + + + + Kompozisyonlar + Grup + Hiçbir grup eklemediniz + Grup ekle + devralındı ​​ + Mülk ekle + Gerekli etiket + Liste görünümünü etkinleştir + İçerik öğesini, alt öğelerinin sıralanabilir ve aranabilir bir listesini gösterecek şekilde yapılandırır, alt öğeler ağaçta gösterilmez + İzin Verilen Şablonlar + Bu tür içerik üzerinde hangi şablon düzenleyicilerinin kullanmasına izin verileceğini seçin + Kök olarak izin ver + Düzenleyicilerin, içerik ağacının kök dizininde bu türden içerik oluşturmasına izin verin. + İzin verilen alt düğüm türleri + Belirtilen türlerdeki içeriğin, bu tür içeriğin altında oluşturulmasına izin verin. + Alt düğümü seçin + Mevcut bir belge türünden sekmeleri ve özellikleri devralın. Mevcut belge türüne yeni sekmeler eklenecek veya aynı ada sahip bir sekme varsa birleştirilecektir. + Bu içerik türü bir bestede kullanıldığından kendi başına oluşturulamaz. + Beste olarak kullanılabilecek içerik türü yok. + Bir kompozisyonun kaldırılması, ilişkili tüm özellik verilerini silecektir. Belge türünü kaydettikten sonra geri dönüş yoktur. + Yeni oluştur + Mevcut olanı kullan + Düzenleyici ayarları + Yapılandırma + Evet, sil + altına taşındı + altına kopyalandı + Taşınacak klasörü seçin + Kopyalanacak klasörü seçin + aşağıdaki ağaç yapısına + Tüm Belge türleri + Tüm Belgeler + Tüm medya öğeleri + bu belge türünün kullanılması kalıcı olarak silinecektir, lütfen bunları da silmek istediğinizi onaylayın. + bu medya türünün kullanılması kalıcı olarak silinecek, lütfen bunları da silmek istediğinizi onaylayın. + bu üye türünün kullanılması kalıcı olarak silinecek, lütfen bunları da silmek istediğinizi onaylayın + ve bu türü kullanan tüm belgeler + ve bu türü kullanan tüm medya öğeleri + ve bu türü kullanan tüm üyeler + Üye düzenleyebilir + Bu özellik değerinin üye tarafından profil sayfasında düzenlenmesine izin ver + Hassas verilerdir + Bu özellik değerini, hassas bilgileri görüntüleme erişimi olmayan içerik düzenleyicilerinden gizleyin + Üye profilinde göster + Bu özellik değerinin üye profil sayfasında görüntülenmesine izin ver + sekmesinde sıralama düzeni yok + Bu beste nerede kullanılıyor? + Bu beste şu anda aşağıdaki içerik türlerinin oluşturulmasında kullanılmaktadır: + Varyasyonlara izin ver + Kültüre göre değişikliklere izin ver + Segmentasyona izin ver + Kültüre göre değişiklik yapın + Segmentlere göre değişiklik yapın + Düzenleyenlerin bu türden içeriği farklı dillerde oluşturmasına izin verin. + Düzenleyenlerin farklı dillerde içerik oluşturmasına izin ver. + Editörlerin bu içeriğin segmentlerini oluşturmasına izin ver. + Kültüre göre değişiklik yapmaya izin ver + Segmentasyona izin ver + Öğe türü + Bir Öğe türüdür + Bir Öğe türü, ağaçta değil, örneğin Yuvalanmış İçerikte kullanılmak üzere tasarlanmıştır. + Bir belge türü, bir veya daha fazla içerik öğesi oluşturmak için kullanıldıktan sonra Öğe türü olarak değiştirilemez. + Bu, bir Öğe türü için geçerli değildir + Bu özellikte değişiklikler yaptınız. Onları atmak istediğinizden emin misiniz? + + + Dil ekle + Zorunlu dil + Düğüm yayınlanmadan önce bu dildeki özelliklerin doldurulması gerekir. + Varsayılan dil + Bir Umbraco sitesinde yalnızca bir varsayılan dil ayarı olabilir. + Varsayılan dili değiştirmek, varsayılan içeriğin kaybolmasına neden olabilir. + Geri döner + Geri dönüş dili yok + Çok dilli içeriğin, istenen dilde yoksa başka bir dile geri dönmesine izin vermek için, buradan seçin. + Geri dönüş dili + yok + + + + Parametre ekle + Parametreyi düzenle + Makro adını girin + Parametreler + Bu makroyu kullanırken mevcut olması gereken parametreleri tanımlayın. + Kısmi görünüm makro dosyasını seçin + + + Model oluşturma + bu biraz zaman alabilir, endişelenmeyin + Oluşturulan modeller + Modeller oluşturulamadı + Model oluşturma başarısız oldu, U günlüğünde istisnaya bakın - 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: + Yedek alanı ekle + Yedek alanı + Varsayılan değer ekle + Varsayılan değer + Yedek alanı + Varsayılan değer + Kasa + Kodlama + Alan seçin + Satır sonlarını dönüştür + Evet, satır sonlarını dönüştür + Satır sonlarını 'br' html etiketiyle değiştirir + Özel Alanlar + Yalnızca tarih + Biçim ve kodlama + Tarih olarak biçimlendir + Değeri, aktif kültüre göre tarih veya saatli tarih olarak biçimlendirin + HTML kodlama + Özel karakterleri HTML eşdeğerleriyle değiştirir. + Alan değerinden sonra eklenecek + Alan değerinden önce eklenecek + Küçük harf + Çıktıyı değiştir + Yok + Çıktı örneği + Alanın sonrasına ekle + Alanın önüne ekle + Özyinelemeli + Evet, yinelemeli yap + Ayırıcı + Standart Alanlar + Büyük harf + URL kodlama + URL'lerdeki özel karakterleri biçimlendirecek + Yalnızca yukarıdaki alan değerleri boş olduğunda kullanılacaktır + Bu alan yalnızca birincil alan boşsa kullanılacaktır + Tarih ve saat - Translation details - Download XML DTD - Fields - Include subpages - Çeviri ayrıntıları + XML DTD'yi İndir + Alanlar + Alt sayfaları dahil et + + - No translator users found. Please create a translator user before you start sending content to translation - The page '%0%' has been send to translation - Send the page '%0%' to translation + + İyi günler! + + Umbraco robotdan teşekkürler + ]]> + + Çevirmen kullanıcısı bulunamadı. Lütfen çeviriye içerik göndermeye başlamadan önce bir çevirmen kullanıcısı oluşturun + '%0%' sayfası çeviriye gönderildi + '%0%' sayfasını çeviriye gönder Toplam kelime - Translate to + Şu dile çevir Ç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ı + Aşağıya tıklayarak yeni çevirdiğiniz sayfaların önizlemesini yapabilirsiniz. Orijinal sayfa bulunursa, 2 sayfanın bir karşılaştırmasını alacaksınız. + Çeviri başarısız oldu, XML dosyası bozulmuş olabilir + Çeviri seçenekleri Çevirmen - Upload translation XML + Çeviri XML'sini yükle - Cache Browser - Çöp Kutusu - Oluşturulan Paketler - Data Tipleri + İçerik + İçerik Şablonları + Medya + Önbellek Tarayıcısı + Geri Dönüşüm Kutusu + Oluşturulan paketler + Veri Türleri Sözlük - Kurulu paketler - Görünüm kur - Başlangıç kiti kur + Yüklü paketler + Dış görünümü yükleyin + Başlangıç ​​kitini yükleyin Diller - Yerel paket yükle + Yerel paketi yükle Makrolar - Medya Tipleri + Medya Türleri Üyeler - Üye Grupları - Roller - Üye Tipleri - Döküman Tipleri + Üye Grupları + Üye Rolleri + Üye Türleri + Belge Türleri + İlişki Türleri Paketler Paketler - Depodan kurulum yap - Install Runway + Kısmi Görünümler + Makro Dosyalarını Kısmi Görüntüle + Depodan yükle + Runway'i Kur Runway modülleri - Scripting Dosyaları - Scriptler - Stil dosyaları + Komut Dosyası Dosyaları + Komut Dosyaları + Stil Sayfaları Şablonlar + Günlük Görüntüleyici + Kullanıcılar + Ayarlar + Şablon Oluşturma + Üçüncü Taraf - 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. + Yeni güncelleme hazır + %0% hazır, indirmek için burayı tıklayın + Sunucuyla bağlantı yok + Güncelleme için hata denetimi. Daha fazla bilgi için lütfen izleme yığınını inceleyin - Yöneticiler + Erişim + Atanan gruplara ve başlangıç ​​düğümlerine bağlı olarak, kullanıcının aşağıdaki düğümlere erişimi vardır + Erişim ata + Yönetici Kategori alanı - Şifreni değiştir + Kullanıcı oluşturuldu + Şifrenizi değiştirin + Fotoğrafı 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 + kilitlenmedi + Şifre değiştirilmedi + Yeni şifreyi onaylayın + Aşağıdaki formu doldurarak ve 'Şifreyi Değiştir' düğmesini tıklayarak Umbraco Arka Ofisine erişim şifrenizi değiştirebilirsiniz İçerik Kanalı + Başka bir kullanıcı oluşturun + Umbraco'ya erişim sağlamak için yeni kullanıcılar oluşturun. Yeni bir kullanıcı oluşturulduğunda, kullanıcıyla paylaşabileceğiniz bir şifre oluşturulacaktır. Açıklama alanı - Kullanıcıyı devre dışı bırak - Döküman Tipi - editör - Alıntı alan + Kullanıcıyı Devre Dışı Bırak + Belge Türü + Düzenleyici + Alıntı alanı + Başarısız giriş denemeleri + Kullanıcı profiline gidin + Erişim ve izinler atamak için gruplar ekleyin + Başka bir kullanıcıyı davet edin + Yeni kullanıcıları Umbraco'ya erişim vermeleri için davet edin. Kullanıcıya, Umbraco'da nasıl oturum açılacağına dair bilgiler içeren bir davet e-postası gönderilecektir. Davetler 72 saat sürer. Dil - Kullanıcı adı - Medya kütüphane düğüm başlatın + Menülerde ve iletişim kutularında göreceğiniz dili ayarlayın + Son kilitlenme tarihi + Son giriş + Parola son değiştirildi + Kullanıcı Adı + Medya başlangıç ​​düğümü + Medya kitaplığını belirli bir başlangıç ​​düğümüyle sınırlayın + Medya başlangıç ​​düğümleri + Medya kitaplığını belirli başlangıç ​​düğümleriyle sınırlayın Bölümler - CMS Erişim devre dışı bırakma - Parola - Şifrenizi sıfırlayın + Umbraco Erişimini Devre Dışı Bırak + henüz giriş yapmadı + Eski şifre + Şifre + Şifreyi sıfırla Şifreniz değiştirildi! - Yeni parolayı onaylayın + Şifre değiştirildi + Lütfen yeni şifreyi 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ı + Yeni şifreniz boş olamaz! + Mevcut şifre + Geçersiz mevcut şifre + Yeni parola ile onaylanan parola arasında bir fark vardı. Lütfen tekrar deneyin! + Onaylanan şifre yeni şifreyle eşleşmiyor! + Alt düğüm izinlerini değiştirin + Şu anda sayfaların izinlerini değiştiriyorsunuz: + İzinlerini değiştirmek için sayfaları seçin + Fotoğrafı kaldır + Varsayılan izinler + Ayrıntılı izinler + Belirli düğümler için izinleri ayarlayın + Profil + Tüm çocukları ara + Kullanıcılara erişim vermek için bölümler ekleyin + Kullanıcı gruplarını seçin + Seçili başlangıç ​​düğümü yok + Seçili başlangıç ​​düğümü yok + İçerik başlangıç ​​düğümü + İçerik ağacını belirli bir başlangıç ​​düğümüyle sınırlayın + İçerik başlangıç ​​düğümleri + İçerik ağacını belirli başlangıç ​​düğümleriyle sınırlayın + Kullanıcının son güncellenme tarihi + oluşturuldu + Yeni kullanıcı başarıyla oluşturuldu. Umbraco'da oturum açmak için aşağıdaki şifreyi kullanın. + Kullanıcı yönetimi + Ad Kullanıcı izinleri - Kullanıcı türü - Kullanıcı tipleri + Kullanıcı grubu + davet edildi + Yeni kullanıcıya, Umbraco'da nasıl oturum açılacağına ilişkin ayrıntıları içeren bir davetiye gönderildi. + Merhabalar, Umbraco'ya hoş geldiniz! Sadece 1 dakika içinde hazır olacaksınız, sadece bir şifre belirlemeniz ve avatarınız için bir resim eklemeniz gerekiyor. + Umbraco'ya hoş geldiniz! Maalesef davetinizin süresi doldu. Lütfen yöneticinizle iletişime geçin ve yeniden göndermesini isteyin. + Kendi fotoğrafınızı yüklemek, diğer kullanıcıların sizi tanımasını kolaylaştıracaktır. Fotoğrafınızı yüklemek için yukarıdaki daireyi tıklayın. Yazar Değiştir Profiliniz - Son tarih + Yakın geçmişiniz Oturum sona eriyor + Kullanıcıyı davet et + Kullanıcı oluştur + Davet gönder + Kullanıcılara dön + Umbraco: Davet + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Merhaba %0%, +

+

+ %1% tarafından Umbraco Arka Ofisine davet edildiniz. +

+

+ %1% tarafından gönderilen mesaj: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Daveti Kabul Etmek İçin Bu Bağlantıyı Tıklayın + +
+
+

Bağlantıya tıklayamazsanız, bu URL'yi kopyalayıp tarayıcı pencerenize yapıştırın:

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

+
+
+


+
+
+ +]]> +
+ Davet Et + Davetiye yeniden gönderiliyor ... + Kullanıcıyı Sil + Bu kullanıcı hesabını silmek istediğinizden emin misiniz? + Tümü + Etkin + Devre Dışı + Kilitlendi + Davet edildi + Etkin Değil + Ad (AZ) + Ad (ZA) + En yeni + En eski + Son giriş + Hiçbir kullanıcı grubu eklenmedi + + + Doğrulama + Doğrulama yok + E-posta adresi olarak doğrula + Sayı olarak doğrula + URL olarak doğrula + ... veya özel bir doğrulama girin + Alan zorunludur + Özel bir doğrulama hata mesajı girin (isteğe bağlı) + Bir normal ifade girin + Özel bir doğrulama hata mesajı girin (isteğe bağlı) + En az eklemeniz gerekiyor + Yalnızca sahip olabilirsiniz + En fazla ekle + + öğeler + url(ler) + url(ler) seçildi + öğe seçildi + Geçersiz tarih + Sayı değil + Geçersiz e-posta + Özel doğrulama + %1% daha fazla gerektirir.]]> + %1% çok fazla.]]> + + + + Değer, önerilen değere ayarlandı: '%0%'. + Değer, '%3%' yapılandırma dosyasındaki XPath '%2%' için '%1%' olarak ayarlandı. + '%3%' yapılandırma dosyasında '%2%' için '%1%' değeri bekleniyordu, ancak '%0%' bulundu. + '%3%' yapılandırma dosyasında '%2%' için beklenmeyen '%0%' değeri bulundu. + + Özel hatalar '%0%' olarak ayarlandı. + Özel hatalar şu anda '%0%' olarak ayarlı. Yayınlanmadan önce bunun '%1%' olarak ayarlanması önerilir. + Özel hatalar başarıyla '%0%' olarak ayarlandı. + Makro Hatalar '%0%' olarak ayarlandı. + MacroErrors, makrolarda herhangi bir hata olması durumunda sitenizdeki bazı veya tüm sayfaların tamamen yüklenmesini önleyecek olan '%0%' olarak ayarlanmıştır. Bunu düzeltmek, değeri "%1%" olarak ayarlayacaktır. + Makro Hatalar artık '%0%' olarak ayarlandı. + + Deneyin IIS Özel Hatalarını Atla '%0%' olarak ayarlandı ve '%1%' IIS sürümünü kullanıyorsunuz. + IIS'yi Atlamayı Deneyin Özel Hataları şu anda '%0%'. IIS sürümünüz (%2%) için bunu '%1%' olarak ayarlamanız önerilir. + IIS Özel Hatalarını Atlamayı deneyin başarıyla '%0%' olarak ayarlandı. + + ​​Dosya mevcut değil: '%0%'. + ​​ '%1%' yapılandırma dosyasında '%0%' bulunamadı.]]> + Bir hata oluştu, tam hata için günlüğü kontrol edin: %0%. + Veritabanı - Veritabanı şeması, Umbraco'nun bu sürümü için doğru + Veritabanı şemanızda %0% sorun algılandı (Ayrıntılar için günlüğe bakın) + Umbraco'nun mevcut sürümüne göre veritabanı şeması doğrulanırken bazı hatalar tespit edildi. + Web sitenizin sertifikası geçerlidir. + Sertifika doğrulama hatası: '%0%' + Web sitenizin SSL sertifikasının süresi doldu. + Web sitenizin SSL sertifikasının süresi %0% gün içinde doluyor. + URL %0% - '%1%' pinglenirken hata oluştu + Şu anda HTTPS şemasını kullanarak siteyi %0% görüntülüyorsunuz. + appSetting 'Umbraco.Core.Https' web.config dosyanızda 'false' olarak ayarlandı. Bu siteye HTTPS şemasını kullanarak eriştiğinizde, bu 'doğru' olarak ayarlanmalıdır. + appSetting 'Umbraco.Core.Https' web.config dosyanızda '%0%' olarak ayarlandı, çerezleriniz%1% güvenli olarak işaretlendi. + Web.config dosyanızdaki 'Umbraco.Core.Https' ayarı güncellenemedi. Hata: %0% + + + HTTPS'yi etkinleştir + web.config dosyasının appSettings öğesinde umbracoSSL ayarını true olarak ayarlar. + appSetting 'Umbraco.Core.Https' artık web.config dosyanızda 'true' olarak ayarlandı, çerezleriniz güvenli olarak işaretlenecek. + Düzelt + Değer karşılaştırma türü 'ShouldNotEqual' olan bir kontrol düzeltilemez. + Sağlanan bir değere sahip 'ShouldEqual' değer karşılaştırma türüne sahip bir kontrol düzeltilemez. + Düzeltilecek değer kontrolü sağlanmadı. + Hata ayıklama derleme modu devre dışı. + Hata ayıklama derleme modu şu anda etkin. Yayınlanmadan önce bu ayarı devre dışı bırakmanız önerilir. + Hata ayıklama derleme modu başarıyla devre dışı bırakıldı. + İzleme modu devre dışı. + İzleme modu şu anda etkin. Yayınlanmadan önce bu ayarı devre dışı bırakmanız önerilir. + İzleme modu başarıyla devre dışı bırakıldı. + Tüm klasörler doğru izinlere sahip. + + %0%]]> + %0% . Yazılmıyorsa herhangi bir işlem yapılmasına gerek yoktur.]]> + Tüm dosyalar doğru izinlere sahip. + + %0%]]> + %0% . Yazılmıyorsa herhangi bir işlem yapılmasına gerek yoktur.]]> + + ​​ X-Frame-Options .]]> + + ​​ X-Frame-Options bulunamadı.]]> + Yapılandırmada Üstbilgiyi Ayarla + Sitenin diğer web siteleri tarafından IFRAMEd olmasını önlemek için web.config’in httpProtocol / customHeaders bölümüne bir değer ekler. + Web.config dosyanıza, sitenin diğer web siteleri tarafından IFRAMEing yapılmasını engelleyen bir başlık oluşturma ayarı eklendi. + web.config dosyası güncellenemedi. Hata: %0% + ​​ X-Content-Type-Options bulundu.]]> + ​​ X-Content-Type-Options bulunamadı.]]> + + MIME koklama güvenlik açıklarına karşı koruma sağlamak için web.config’in httpProtocol / customHeaders bölümüne bir değer ekler. + Web.config dosyanıza MIME koklama güvenlik açıklarına karşı koruma sağlayan bir üstbilgi oluşturma ayarı eklendi. + ​​ Strict-Transport-Security başlığı bulundu.]]> + ​​ Strict-Transport-Security başlığı bulunamadı.]]> + Web.config'in httpProtocol / customHeaders bölümüne 'max-age=10886400' değerine sahip 'Strict-Transport-Security' başlığını ekler. Bu düzeltmeyi yalnızca alanlarınız önümüzdeki 18 hafta boyunca (minimum) https ile çalışacaksa kullanın. + HSTS başlığı web.config dosyanıza eklendi. + ​​ X-XSS-Protection başlığı bulundu.]]> + ​​ X-XSS-Protection başlığı bulunamadı.]]> + 'X-XSS-Protection' başlığını '1; mode=block 'web.config'in httpProtocol / customHeaders bölümüne. + X-XSS-Protection başlığı web.config dosyanıza eklendi. + + ​​%0%.]]> + ​​Web sitesi teknolojisi hakkında bilgi veren hiçbir başlık bulunamadı. + ​​Web.config dosyasında system.net/mailsettings bulunamadı. + Web.config dosyası system.net/mailsettings bölümünde, ana bilgisayar yapılandırılmamış. + SMTP ayarları doğru yapılandırıldı ve hizmet beklendiği gibi çalışıyor. + '%0%' ana bilgisayarı ve '%1%' bağlantı noktası ile yapılandırılan SMTP sunucusuna ulaşılamadı. Lütfen Web.config dosyasındaki system.net/mailsettings içindeki SMTP ayarlarının doğruluğunu kontrol edin. + %0% olarak ayarlandı]]> + %0%.]]> +

%0% tarihinde %1% ile çalıştırılan planlanmış Umbraco Sağlık Kontrollerinin sonuçları aşağıdaki gibidir:

%2%]]>
+ Umbraco Sağlık Kontrolü Durumu: %0% + Tüm Grupları Kontrol Edin + Grubu kontrol et + + Durum denetleyicisi, sitenizin çeşitli alanlarını en iyi uygulama ayarları, yapılandırma, olası sorunlar vb. için değerlendirir. Sorunları bir düğmeye basarak kolayca düzeltebilirsiniz. + Kendi sağlık kontrollerinizi ekleyebilir, özel durum kontrolleri hakkında daha fazla bilgi için belgeler .

+ ]]> +
+ + + URL izleyiciyi devre dışı bırakın + URL izleyiciyi etkinleştir + Orijinal URL + Yönlendirildi + URL Yönetimini Yeniden Yönlendir + Aşağıdaki URL'ler bu içerik öğesine yönlendiriyor: + Yönlendirme yapılmadı + Yayınlanan bir sayfa yeniden adlandırıldığında veya taşındığında, yeni sayfaya otomatik olarak bir yönlendirme yapılır. + '%0%' dan '%1%' e yönlendirmeyi kaldırmak istediğinizden emin misiniz? + Yönlendirme URL'si kaldırıldı. + Yönlendirme URL'sini kaldırma hatası. + Bu, yönlendirmeyi kaldırır + URL izleyiciyi devre dışı bırakmak istediğinizden emin misiniz? + URL izleyici artık devre dışı bırakıldı. + URL izleyici devre dışı bırakılırken hata oluştu, günlük dosyanızda daha fazla bilgi bulunabilir. + URL izleyici artık etkinleştirildi. + URL izleyici etkinleştirilirken hata oluştu, günlük dosyanızda daha fazla bilgi bulunabilir. + + + Aralarından seçim yapabileceğiniz Sözlük öğesi yok + + + %0% karakter kaldı.]]> + %1% çok fazla.]]> + + + Çöp kutusuna gönderilmiş içerik: {0} Şu kimliğe sahip orijinal ana içerikle ilgili: {1} + Şu kimliğe sahip çöp kutusuna gönderilen medya: {0} Şu kimliğe sahip orijinal ana medya öğesiyle ilgili: {1} + Bu öğe otomatik olarak geri yüklenemez + Bu öğenin otomatik olarak geri yüklenebileceği bir yer yok. Aşağıdaki ağacı kullanarak öğeyi manuel olarak taşıyabilirsiniz. + altında geri yüklendi , + + + Yön + Ebeveynden alt öğeye + Çift yönlü + Üst + Çocuk + Sayım + İlişkiler + Oluşturuldu + Yorum + Adı + Bu ilişki türü için ilişki yok. + İlişki Türü + İlişkiler + + + Başlarken + Yeniden Yönlendirme URL Yönetimi + İçerik + Hoş Geldiniz + Yönetimi İnceleyin + Yayınlanma Durumu + Model Oluşturucu + Durum Kontrolü + Profil oluşturma + Başlarken + Umbraco Formlarını Yükleyin + + + Geri dön + Etkin düzen: + Git + grup + geçti + uyarı + başarısız oldu + öneri + Kontrol geçti + Kontrol başarısız oldu + Arka ofis aramasını aç + Backoffice yardımını Aç / Kapat + Profil seçeneklerinizi açın / kapatın + %0% için Kurulum Kültürü ve Ana Bilgisayar Adları + %0% altında yeni düğüm oluştur + %0% üzerinde genel erişim kurun + %0% için Kurulum İzinleri + %0% için sıralama düzenini değiştir + %0% temelinde İçerik Şablonu oluşturun + için bağlam menüsünü aç + Mevcut dil + Dili değiştir + Yeni klasör oluştur + Kısmi Görünüm + Kısmi Görünüm Makrosu + Üye + Veri türü + Yönlendirme kontrol panelinde ara + Kullanıcı grubu bölümünde ara + Kullanıcılar bölümünde ara + Öğe oluştur + Oluştur + Düzenle + Reklam + Yeni satır ekle + Diğer seçenekleri görüntüleyin + Çeviri var + Eksik çeviri + Sözlük öğeleri + + + Referanslar + Bu Veri Türünde referans yok. + Belge Türlerinde Kullanılır + Belge Türlerine referans yok. + Medya Türlerinde Kullanılır + Medya Türlerine referans yok. + Üye Türlerinde Kullanılır + Üye Türlerine referans yok. + Kullanan + Belgelerde Kullanıldı + Üyelerde Kullanıldı + Medyada Kullanıldı + + + Kaydedilmiş Aramayı Sil + Günlük Düzeyleri + Hepsini seç + Tüm seçimleri kaldır + Kaydedilmiş Aramalar + Aramayı Kaydet + Arama sorgunuz için kolay bir ad girin + Aramayı Filtrele + Toplam Öğeler + Zaman damgası + Seviye + Makine + Mesaj + İstisna + Özellikler + Google ile Ara + Bu mesajı Google ile ara + Bing ile Ara + Bu iletiyi Bing ile ara + Umbraco'yu Arayın + Bu iletiyi Umbraco forumlarımızda ve belgelerimizde arayın + Google ile Umbraco'muzda arama yapın + Umbraco forumlarımızda Google'ı kullanarak arama yapın + Umbraco Kaynağını Ara + Github'da Umbraco kaynak kodu içinde arama + Umbraco Sorunlarını Ara + Github'da Umbraco Sorunlarını Ara + Bu aramayı sil + İstek Kimliği Olan Günlükleri Bul + Ad Alanına Sahip Günlükleri Bul + Makine Adına Sahip Günlükleri Bul + + + + %0% Kopyala + %0%, %1%'den + Tüm öğeleri kaldır + Panoyu temizle + + + Mülk Eylemlerini Aç + Özellik Eylemlerini Kapat + + + Bekle + Durumu yenile + Bellek Önbelleği + + + + Yeniden yükle + Veritabanı Önbelleği + + Yeniden oluşturmak pahalı olabilir. + Yeniden yükleme yeterli olmadığında ve veritabanı önbelleğinin yüklenmediğini düşündüğünüzde kullanın. + uygun şekilde oluşturulmuş— bu bazı kritik Umbraco sorunlarına işaret eder. + ]]> + + Yeniden Oluştur + Dahili + + gerekmez. + ]]> + + Topla + Yayınlanmış Önbellek Durumu + Önbellekler + + + Performans profili oluşturma + + + Umbraco şu anda hata ayıklama modunda çalışıyor. Bu, sayfaları işlerken performansı değerlendirmek için yerleşik performans profilleyicisini kullanabileceğiniz anlamına gelir. +

+

+ Profil oluşturucuyu belirli bir sayfa oluşturma için etkinleştirmek istiyorsanız, sayfayı talep ederken sorgu dizesine umbDebug=true eklemeniz yeterlidir. +

+

+ Profilcinin tüm sayfa görüntülemeleri için varsayılan olarak etkinleştirilmesini istiyorsanız, aşağıdaki geçişi kullanabilirsiniz. + Tarayıcınızda, profil oluşturucuyu otomatik olarak etkinleştiren bir çerez ayarlayacaktır. + Başka bir deyişle, profil oluşturucu yalnızca tarayıcınızda varsayılan olarak etkin olacaktır - diğer herkesin değil. +

+ ]]> +
+ Profil oluşturucuyu varsayılan olarak etkinleştirin + Kolay hatırlatma + + + Bir üretim sitesinin hata ayıklama modunda çalışmasına asla izin vermemelisiniz. Web.config içindeki <compilation /> öğesinde debug="false" ayarlanarak hata ayıklama modu kapatılır. +

+ ]]> +
+ + + Umbraco şu anda hata ayıklama modunda çalışmadığından yerleşik profil oluşturucuyu kullanamazsınız. Bir üretim sahası için böyle olması gerekir. +

+

+ Hata ayıklama modu, web.config'deki <compilation /> öğesinde debug="true" ayarlanarak etkinleştirilir. +

+ ]]> +
+ + + Umbraco eğitim videolarının saatleri yalnızca bir tıklama uzaklıkta + + Umbraco'da ustalaşmak mı istiyorsunuz? Umbraco'nun kullanımıyla ilgili bu videolardan birini izleyerek en iyi uygulamaları öğrenmek için birkaç dakikanızı ayırın. Daha da fazla Umbraco videosu için umbraco.tv adresini ziyaret edin

+ ]]> +
+ Başlamak için + + + Buradan başlayın + Bu bölüm, Umbraco siteniz için yapı taşlarını içerir. Ayarlar bölümündeki öğelerle çalışma hakkında daha fazla bilgi edinmek için aşağıdaki bağlantıları izleyin + Daha fazla bilgi edinin + + öğelerle çalışma hakkında daha fazla bilgi edinin Our Umbraco'nun Dokümantasyon bölümünde + ]]> + + + Topluluk Forumu'nda bir soru sorun + ]]> + + + Eğitici videolarımızı izleyin (bazıları ücretsiz, bazıları abonelik gerektirir) + ]]> + + + Üretkenliği artıran araçlarımız ve ticari desteğimiz hakkında bilgi edinin + ]]> + + + eğitim ve sertifika fırsatları hakkında bilgi edinin + ]]> + + + + Dost Canlısı CMS'e Hoş Geldiniz + Umbraco'yu seçtiğiniz için teşekkür ederiz - bunun güzel bir şeyin başlangıcı olabileceğini düşünüyoruz. İlk başta bunaltıcı gibi görünse de, öğrenme eğrisini olabildiğince sorunsuz ve hızlı hale getirmek için çok şey yaptık. + + + Umbraco Formları + Sezgisel bir sürükle ve bırak arayüzü kullanarak formlar oluşturun. E-postalar gönderen basit iletişim formlarından CRM sistemleriyle entegre olan gelişmiş anketlere kadar. Müşterileriniz buna bayılacak! + + + Yeni blok oluştur + Bir ayarlar bölümü ekleyin + Görünümü seçin + Stil sayfasını seçin + Küçük resim seçin + Yeni oluştur + Özel stil sayfası + Stil sayfası ekle + Düzenleyici görünümü + Veri modelleri + Katalog görünümü + Arka plan rengi + Simge rengi + İçerik modeli + Etiket + Özel görünüm + Ayarlar modeli + Yer paylaşımı düzenleyici boyutu + Özel görünüm ekle + Ayarları ekle + Etiket şablonunun üzerine yaz + %0% içeriğini silmek istediğinizden emin misiniz?]]> + %0% blok yapılandırmasını silmek istediğinizden emin misiniz?]]> + Bu bloğun içeriği hala mevcut olacak, bu içeriğin düzenlenmesi artık kullanılamayacak ve desteklenmeyen içerik olarak gösterilecek. + + Küçük Resim + Küçük resim ekle + Boş oluştur + Pano + Ayarlar + Gelişmiş + İçerik düzenleyiciyi gizlemeye zorla + Bu içerikte değişiklikler yaptınız. Onları atmak istediğinizden emin misiniz? + Oluşturma iptal edilsin mi? + + Hata! + Bu bloğun ElementType'ı artık mevcut değil + '%0%' özelliği, bloklarda desteklenmeyen '%1%' düzenleyicisini kullanıyor. + + + İçerik Şablonları Nedir? + İçerik Şablonları, yeni bir içerik düğümü oluştururken seçilebilen önceden tanımlanmış içeriklerdir. + Nasıl İçerik Şablonu oluşturabilirim? + + İçerik Şablonu oluşturmanın iki yolu vardır:

+
    +
  • Bir içerik düğümünü sağ tıklayın ve yeni bir İçerik Şablonu oluşturmak için "İçerik Şablonu Oluştur" u seçin.
  • +
  • Ayarlar bölümündeki İçerik Şablonları ağacını sağ tıklayın ve İçerik Şablonu oluşturmak istediğiniz Belge Türünü seçin.
  • +
+

Bir ad verildiğinde, editörler İçerik Şablonunu yeni sayfalarının temeli olarak kullanmaya başlayabilir.

+ ]]> +
+ İçerik Şablonlarını nasıl yönetirim? + Ayarlar bölümündeki "İçerik Şablonları" ağacından İçerik Şablonlarını düzenleyebilir ve silebilirsiniz. İçerik Şablonunun dayandığı Belge Türünü genişletin ve düzenlemek veya silmek için tıklayın. diff --git a/TestSite/Umbraco/Config/Lang/zh.xml b/TestSite/Umbraco/Config/Lang/zh.xml index a8dd8a8..113845a 100644 --- a/TestSite/Umbraco/Config/Lang/zh.xml +++ b/TestSite/Umbraco/Config/Lang/zh.xml @@ -90,32 +90,12 @@ 保存并发布 保存并提交审核 保存列表视图 - 预览 + 预览 因未设置模板无法预览 选择样式 显示样式 插入表格 - - 要更改所选节点的文档类型,先在列表中选择合适的文档类型。 - 然后设置当前文档类型到新文档类型的各字段间的对应映射关系并保存。 - 内容已被重新发布 - 当前属性 - 当前类型 - 不能改变文档类型,因为没有可替代的类型。 - 文档类型已更改 - 要映射的字段 - 映射字段 - 新模板 - 新类型 - - 内容 - 选择新的文档类型 - 选中文档的类型已被成功更改为[new type],以下字段被映射: - - 不能完成字段映射,因为存在一个字段映射至多字段的问题。 - 仅显示可作为替代的文档类型。 - 已发布 关于本页 @@ -146,8 +126,8 @@ 属性 该文档不可见,因为其上级 '%0%' 未发布。 该文档已发布,但是没有更新至缓存(内部错误) - Could not get the url - This document is published but its url would collide with content %0% + Could not get the URL + This document is published but its URL would collide with content %0% 发布 发布状态 发布于 @@ -169,7 +149,7 @@ 子项 目标 这将转换到服务器上的以下时间: - 这是什么意思?]]> + 这是什么意思?]]> 点击上传 @@ -257,8 +237,8 @@ 正在清空回收站,请不要关闭窗口。 回收站已清空 从回收站删除的项目将不可恢复 - regexlib.com的服务暂时出现问题。]]> - 查找正则表达式来验证输入,如: 'email、'zip-code'、'url'。 + regexlib.com的服务暂时出现问题。]]> + 查找正则表达式来验证输入,如: 'email、'zip-code'、'URL'。 移除宏 必填项 站点已重建索引 @@ -514,7 +494,7 @@ 数据库未找到!请检查数据库连接串设置。

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

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

]]>
@@ -562,7 +542,7 @@ 我要从头开始 如何操作?) + (如何操作?) 您也可以安装晚一些安装“Runway”。 ]]> 您刚刚安装了一个干净的系统,要继续吗? @@ -630,7 +610,7 @@ 在下方登录 登录 会话超时 - © 2001 - %0%
Umbraco.com

]]>
+ © 2001 - %0%
Umbraco.com

]]>
忘记密码? 电子邮件将发送到地址指定的链接, 以重置您的密码 如果电子邮件与我们的记录相符, 则将发送带有密码重置指令的邮件 @@ -847,7 +827,6 @@ 选项卡 主控文档类型激活 该文档类型使用 - 作为主控文档类型. 主控文档类型的标签只能在主控文档类型里修改。 没有字段设置在该标签页 添加图标 @@ -1156,7 +1135,7 @@ 验证 验证为电子邮件 验证为数字 - 验证为 url + 验证为 URL ...或输入自定义验证 字段是强制性的 @@ -1245,19 +1224,23 @@ %0%.]]> - 禁用 url 跟踪程序 - 启用 url 跟踪程序 + 禁用 URL 跟踪程序 + 启用 URL 跟踪程序 原始网址 已重定向至 未进行重定向 当已发布的页重命名或移动时, 将自动对新页进行重定向。 确实要删除 "%0%" 到 "%1%" 的重定向吗? 重定向URL已删除。 - 删除重定向 url 时出错. - 是否确实要禁用 url 跟踪程序? - url 跟踪器现在已被禁用。 - 禁用 url 跟踪程序时出错, 可以在日志文件中找到更多信息。 - 现在已启用 url 跟踪程序。 - 启用 url 跟踪程序时出错, 可以在日志文件中找到更多信息。 + 删除重定向 URL 时出错. + 是否确实要禁用 URL 跟踪程序? + URL 跟踪器现在已被禁用。 + 禁用 URL 跟踪程序时出错, 可以在日志文件中找到更多信息。 + 现在已启用 URL 跟踪程序。 + 启用 URL 跟踪程序时出错, 可以在日志文件中找到更多信息。 + + + 全选 + 取消全选 diff --git a/TestSite/Umbraco/Config/Lang/zh_tw.xml b/TestSite/Umbraco/Config/Lang/zh_tw.xml index bac817a..438458f 100644 --- a/TestSite/Umbraco/Config/Lang/zh_tw.xml +++ b/TestSite/Umbraco/Config/Lang/zh_tw.xml @@ -87,32 +87,12 @@ 保存並發佈 保存並提交審核 保存清單檢視 - 預覽 + 預覽 因未設置範本無法預覽 選擇樣式 顯示樣式 插入表格 - - 要更改所選節點的文檔類型,先在列表中選擇合適的文檔類型。 - 然後設置當前文檔類型到新文檔類型的各欄位間的對應映射關係並保存。 - 內容已被重新發佈 - 當前屬性 - 當前類型 - 不能改變文檔類型,因為沒有可替代的類型。 - 文檔類型已更改 - 要映射的欄位 - 映射欄位 - 新範本 - 新類型 - - 內容 - 選擇新的文檔類型 - 選中文檔的類型已被成功更改為[new type],以下欄位被映射: - - 不能完成欄位映射,因為存在一個欄位映射至多欄位的問題。 - 僅顯示可作為替代的文檔類型。 - 已發表 關於本頁 @@ -166,7 +146,7 @@ 子項目 目標 預計發表的時間(伺服器端) - 這是什麼意思?]]> + 這是什麼意思?]]> 點選以便上傳 @@ -254,8 +234,8 @@ 正在清空回收站,請不要關閉窗口。 回收站已清空 從回收站刪除的項目將不可恢復 - regexlib.com的網站服務目前出現些狀況,而我們無能為力。我們對此不便感到十分抱歉。]]> - 查找規則運算式來驗證輸入,如: 'email、'zip-code'、'url'。 + regexlib.com的網站服務目前出現些狀況,而我們無能為力。我們對此不便感到十分抱歉。]]> + 查找規則運算式來驗證輸入,如: 'email、'zip-code'、'URL'。 移除巨集 必填項目 網站已重建索引 @@ -507,7 +487,7 @@

請編輯檔案"web.config" (例如使用Visual Studio或您喜歡的編輯器),移動到檔案底部,並在名稱為"UmbracoDbDSN"的字串中設定資料庫連結資訊,並存檔。

點選重試按鈕當上述步驟完成。
- + 在此查詢更多編輯web.config的資訊。

]]>
若需要時,請聯繫您的網路公司。如果您在本地機器或伺服器安裝的話,您也許需要聯絡系統管理者。]]> @@ -553,7 +533,7 @@ 我要從頭開始 學習該怎麼做) + (學習該怎麼做) 您晚點仍可以選擇安裝Runway,請至開發者區域選擇安裝。 ]]> 您剛剛安裝了一個乾淨的系統,要繼續嗎? @@ -619,7 +599,7 @@ 下方登入 登入使用 連線時間過了 - © 2001 - %0%
Umbraco.com

]]>
+ © 2001 - %0%
Umbraco.com

]]>
忘記密碼? 一封內有重設密碼連結的電子郵件已經寄出給您 一封內有重設密碼連結的電子郵件已經寄到此信箱 @@ -834,7 +814,6 @@ 選項卡 主控文件類型啟動 該文檔類型使用 - 作為主控文件類型. 主控文件類型的標籤只能在主控文件類型裡修改。 沒有欄位設置在該標籤頁 增加圖示 @@ -1240,4 +1219,8 @@ 轉址追蹤器已開啟。 啟動轉址追蹤器錯誤,更多資訊請參閱您的紀錄檔。 + + 全選 + 取消全選 + diff --git a/TestSite/Umbraco/Js/app.js b/TestSite/Umbraco/Js/app.js index 74a7008..645296f 100644 --- a/TestSite/Umbraco/Js/app.js +++ b/TestSite/Umbraco/Js/app.js @@ -91,6 +91,6 @@ angular.module("umbraco.viewcache", []) // be able to configure angular values in the Default.cshtml // view which is much easier to do that configuring values by injecting them in the back office controller // to follow through to the js initialization stuff -if (angular.isFunction(document.angularReady)) { +if (_.isFunction(document.angularReady)) { document.angularReady.apply(this, [app]); } diff --git a/TestSite/Umbraco/Js/init.js b/TestSite/Umbraco/Js/init.js index d5c5166..ce824f6 100644 --- a/TestSite/Umbraco/Js/init.js +++ b/TestSite/Umbraco/Js/init.js @@ -34,7 +34,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', else { const introTourShown = localStorageService.get("introTourShown"); - if(!introTourShown){ + if (!introTourShown) { // Go & show email marketing tour (ONLY when intro tour is completed or been dismissed) tourService.getTourByAlias("umbEmailMarketing").then(function (emailMarketingTour) { // Only show the email marketing tour one time - dismissing it or saying no will make sure it never appears again @@ -45,7 +45,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', // Only show the email tour once per logged in session // The localstorage key is removed on logout or user session timeout const emailMarketingTourShown = localStorageService.get("emailMarketingTourShown"); - if(!emailMarketingTourShown){ + if (!emailMarketingTourShown) { tourService.startTour(emailMarketingTour); localStorageService.set("emailMarketingTourShown", true); } @@ -89,7 +89,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', currentRouteParams = toRetain; } else { - currentRouteParams = angular.copy(current.params); + currentRouteParams = Utilities.copy(current.params); } @@ -183,7 +183,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', currentRouteParams = toRetain; } else { - currentRouteParams = angular.copy(next.params); + currentRouteParams = Utilities.copy(next.params); } //always clear the 'sr' query string (soft redirect) if it exists @@ -191,7 +191,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', currentRouteParams.sr = null; $route.updateParams(currentRouteParams); } - + } } }); diff --git a/TestSite/Umbraco/Js/install.loader.js b/TestSite/Umbraco/Js/install.loader.js index 997c8cb..8c17b8c 100644 --- a/TestSite/Umbraco/Js/install.loader.js +++ b/TestSite/Umbraco/Js/install.loader.js @@ -1,6 +1,6 @@ LazyLoad.js([ 'lib/jquery/jquery.min.js', - + 'lib/angular/angular.js', 'lib/angular-cookies/angular-cookies.js', 'lib/angular-touch/angular-touch.js', @@ -8,10 +8,14 @@ LazyLoad.js([ 'lib/angular-messages/angular-messages.js', 'lib/angular-aria/angular-aria.min.js', 'lib/underscore/underscore-min.js', - 'lib/angular-ui-sortable/sortable.js', + 'lib/angular-ui-sortable/sortable.js', + + 'js/utilities.js', + 'js/installer.app.js', 'js/umbraco.directives.js', 'js/umbraco.installer.js' + ], function () { jQuery(document).ready(function () { angular.bootstrap(document, ['umbraco']); diff --git a/TestSite/Umbraco/Js/main.controller.js b/TestSite/Umbraco/Js/main.controller.js index 883907d..bc10dfa 100644 --- a/TestSite/Umbraco/Js/main.controller.js +++ b/TestSite/Umbraco/Js/main.controller.js @@ -1,15 +1,14 @@ - /** * @ngdoc controller * @name Umbraco.MainController * @function - * + * * @description * The main application controller - * + * */ -function MainController($scope, $location, appState, treeService, notificationsService, - userService, historyService, updateChecker, navigationService, eventsService, +function MainController($scope, $location, appState, treeService, notificationsService, + userService, historyService, updateChecker, navigationService, eventsService, tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) { //the null is important because we do an explicit bool check on this in the view @@ -28,17 +27,21 @@ function MainController($scope, $location, appState, treeService, notificationsS assetsService.loadJs(tinyJsAsset, $scope); }); - // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { if (evt.keyCode === 9) { - $scope.tabbingActive = true; - $scope.$digest(); - window.removeEventListener('keydown', handleFirstTab); - window.addEventListener('mousedown', disableTabbingActive); + enableTabbingActive(); } } - + + function enableTabbingActive() { + $scope.tabbingActive = true; + $scope.$digest(); + window.addEventListener('mousedown', disableTabbingActive); + window.removeEventListener("keydown", handleFirstTab); + } + function disableTabbingActive(evt) { $scope.tabbingActive = false; $scope.$digest(); @@ -48,6 +51,12 @@ function MainController($scope, $location, appState, treeService, notificationsS window.addEventListener("keydown", handleFirstTab); + $scope.$on("showFocusOutline", function() { + $scope.tabbingActive = true; + window.addEventListener('mousedown', disableTabbingActive); + window.removeEventListener("keydown", handleFirstTab); + }); + $scope.removeNotification = function (index) { notificationsService.remove(index); @@ -57,12 +66,14 @@ function MainController($scope, $location, appState, treeService, notificationsS appState.setSearchState("show", false); }; - $scope.showLoginScreen = function(isTimedOut) { + $scope.showLoginScreen = function (isTimedOut) { + $scope.login.pageTitle = $scope.$root.locationTitle; $scope.login.isTimedOut = isTimedOut; $scope.login.show = true; }; - $scope.hideLoginScreen = function() { + $scope.hideLoginScreen = function () { + $scope.$root.locationTitle = $scope.login.pageTitle; $scope.login.show = false; }; @@ -73,6 +84,7 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; + $scope.showLoginScreen(isTimedOut); // Remove the localstorage items for tours shown @@ -106,7 +118,8 @@ function MainController($scope, $location, appState, treeService, notificationsS message: "Click to download", sticky: true, type: "info", - url: update.url + url: update.url, + target: "_blank" }; notificationsService.add(notification); } @@ -173,7 +186,7 @@ function MainController($scope, $location, appState, treeService, notificationsS evts.push(eventsService.on("appState.overlay", function (name, args) { $scope.overlay = args; })); - + // events for tours evts.push(eventsService.on("appState.tour.start", function (name, args) { $scope.tour = args; diff --git a/TestSite/Umbraco/Js/navigation.controller.js b/TestSite/Umbraco/Js/navigation.controller.js index 281be2d..9f5a6e4 100644 --- a/TestSite/Umbraco/Js/navigation.controller.js +++ b/TestSite/Umbraco/Js/navigation.controller.js @@ -68,7 +68,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar args.event.stopPropagation(); args.event.preventDefault(); - if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { + if (n.metaData && n.metaData["jsClickCallback"] && Utilities.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { //this is a legacy tree node! var jsPrefix = "javascript:"; var js; @@ -142,7 +142,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar var isInit = false; var evts = []; - + //Listen for global state changes evts.push(eventsService.on("appState.globalState.changed", function (e, args) { if (args.key === "showNavigation") { @@ -200,7 +200,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $scope.treeApi.load({ section: $scope.currentSection, customTreeParams: $scope.customTreeParams, cacheKey: $scope.treeCacheKey }); }); } - + //show/hide search results if (args.key === "showSearchResults") { $scope.showSearchResults = args.value; @@ -222,7 +222,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } else { $location.search("mculture", null); } - + var currentEditorState = editorState.getCurrent(); if (currentEditorState && currentEditorState.path) { $scope.treeApi.syncTree({ path: currentEditorState.path, activate: true }); @@ -233,13 +233,13 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //Emitted when a language is created or an existing one saved/edited evts.push(eventsService.on("editors.languages.languageSaved", function (e, args) { - if(args.isNew){ + if (args.isNew) { //A new language has been created - reload languages for tree loadLanguages().then(function (languages) { $scope.languages = languages; }); } - else if(args.language.isDefault){ + else if (args.language.isDefault) { //A language was saved and was set to be the new default (refresh the tree, so its at the top) loadLanguages().then(function (languages) { $scope.languages = languages; @@ -282,7 +282,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar /** * For multi language sites, this ensures that mculture is set to either the last selected language or the default one - */ + */ function ensureMainCulture() { if ($location.search().mculture) { return; @@ -295,7 +295,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $timeout(function () { $scope.selectLanguage(language); }); - } + } /** * Based on the current state of the application, this configures the scope variables that control the main tree and language drop down @@ -325,6 +325,9 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar var queryParams = {}; if ($scope.selectedLanguage && $scope.selectedLanguage.culture) { queryParams["culture"] = $scope.selectedLanguage.culture; + if (!mainCulture) { + $location.search("mculture", $scope.selectedLanguage.culture); + } } var queryString = $.param(queryParams); //create the query string from the params object } @@ -432,7 +435,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //the nav is ready, let the app know eventsService.emit("app.navigationReady", { treeApi: $scope.treeApi }); - + } }); }); @@ -469,7 +472,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar // add the selected culture to a cookie so the user will log back into the same culture later on (cookie lifetime = one year) var expireDate = new Date(); expireDate.setDate(expireDate.getDate() + 365); - $cookies.put("UMB_MCULTURE", language.culture, {path: "/", expires: expireDate}); + $cookies.put("UMB_MCULTURE", language.culture, { path: "/", expires: expireDate }); // close the language selector $scope.page.languageSelectorIsOpen = false; @@ -495,9 +498,10 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //execute them sequentially // set selected language to active - angular.forEach($scope.languages, function(language){ + Utilities.forEach($scope.languages, language => { language.active = false; }); + language.active = true; angularHelper.executeSequentialPromises(promises); @@ -538,7 +542,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar closeTree(); }; - $scope.onOutsideClick = function() { + $scope.onOutsideClick = function () { closeTree(); }; diff --git a/TestSite/Umbraco/Js/routes.js b/TestSite/Umbraco/Js/routes.js index 556a4d6..9312279 100644 --- a/TestSite/Umbraco/Js/routes.js +++ b/TestSite/Umbraco/Js/routes.js @@ -154,7 +154,7 @@ app.config(function ($routeProvider) { //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 replace the template dynamically based on the current tree. - controller: function ($scope, $routeParams, treeService) { + controller: function ($scope, $routeParams, navigationService) { if (!$routeParams.method) { $scope.templateUrl = "views/common/dashboard.html"; @@ -176,24 +176,7 @@ app.config(function ($routeProvider) { $scope.templateUrl = "views/users/overview.html"; return; } - - // Here we need to figure out if this route is for a user's package tree and if so then we need - // to change it's convention view path to: - // /App_Plugins/{mypackage}/backoffice/{treetype}/{method}.html - - // otherwise if it is a core tree we use the core paths: - // views/{treetype}/{method}.html - - var packageTreeFolder = treeService.getTreePackageFolder($routeParams.tree); - - if (packageTreeFolder) { - $scope.templateUrl = (Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + - "/" + packageTreeFolder + - "/backoffice/" + $routeParams.tree + "/" + $routeParams.method + ".html"); - } - else { - $scope.templateUrl = ('views/' + $routeParams.tree + '/' + $routeParams.method + '.html'); - } + $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method); }, reloadOnSearch: false, resolve: canRoute(true) @@ -202,30 +185,13 @@ app.config(function ($routeProvider) { //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 replace the template dynamically based on the current tree. - controller: function ($scope, $route, $routeParams, treeService) { + controller: function ($scope, $routeParams, navigationService) { if (!$routeParams.tree || !$routeParams.method) { $scope.templateUrl = "views/common/dashboard.html"; + return; } - - // Here we need to figure out if this route is for a package tree and if so then we need - // to change it's convention view path to: - // /App_Plugins/{mypackage}/backoffice/{treetype}/{method}.html - - // otherwise if it is a core tree we use the core paths: - // views/{treetype}/{method}.html - - var packageTreeFolder = treeService.getTreePackageFolder($routeParams.tree); - - if (packageTreeFolder) { - $scope.templateUrl = (Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + - "/" + packageTreeFolder + - "/backoffice/" + $routeParams.tree + "/" + $routeParams.method + ".html"); - } - else { - $scope.templateUrl = ('views/' + $routeParams.tree + '/' + $routeParams.method + '.html'); - } - + $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method); }, reloadOnSearch: false, reloadOnUrl: false, diff --git a/TestSite/Umbraco/Js/umbraco.controllers.js b/TestSite/Umbraco/Js/umbraco.controllers.js index b5f3a3d..57e1171 100644 --- a/TestSite/Umbraco/Js/umbraco.controllers.js +++ b/TestSite/Umbraco/Js/umbraco.controllers.js @@ -4,10 +4,10 @@ * @ngdoc controller * @name Umbraco.MainController * @function - * + * * @description * The main application controller - * + * */ function MainController($scope, $location, appState, treeService, notificationsService, userService, historyService, updateChecker, navigationService, eventsService, tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) { //the null is important because we do an explicit bool check on this in the view @@ -24,16 +24,19 @@ tinyMceAssets.forEach(function (tinyJsAsset) { assetsService.loadJs(tinyJsAsset, $scope); }); - // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { if (evt.keyCode === 9) { - $scope.tabbingActive = true; - $scope.$digest(); - window.removeEventListener('keydown', handleFirstTab); - window.addEventListener('mousedown', disableTabbingActive); + enableTabbingActive(); } } + function enableTabbingActive() { + $scope.tabbingActive = true; + $scope.$digest(); + window.addEventListener('mousedown', disableTabbingActive); + window.removeEventListener('keydown', handleFirstTab); + } function disableTabbingActive(evt) { $scope.tabbingActive = false; $scope.$digest(); @@ -41,6 +44,11 @@ window.addEventListener('keydown', handleFirstTab); } window.addEventListener('keydown', handleFirstTab); + $scope.$on('showFocusOutline', function () { + $scope.tabbingActive = true; + window.addEventListener('mousedown', disableTabbingActive); + window.removeEventListener('keydown', handleFirstTab); + }); $scope.removeNotification = function (index) { notificationsService.remove(index); }; @@ -48,10 +56,12 @@ appState.setSearchState('show', false); }; $scope.showLoginScreen = function (isTimedOut) { + $scope.login.pageTitle = $scope.$root.locationTitle; $scope.login.isTimedOut = isTimedOut; $scope.login.show = true; }; $scope.hideLoginScreen = function () { + $scope.$root.locationTitle = $scope.login.pageTitle; $scope.login.show = false; }; var evts = []; @@ -87,7 +97,8 @@ message: 'Click to download', sticky: true, type: 'info', - url: update.url + url: update.url, + target: '_blank' }; notificationsService.add(notification); } @@ -232,7 +243,7 @@ var n = args.node; args.event.stopPropagation(); args.event.preventDefault(); - if (n.metaData && n.metaData['jsClickCallback'] && angular.isString(n.metaData['jsClickCallback']) && n.metaData['jsClickCallback'] !== '') { + if (n.metaData && n.metaData['jsClickCallback'] && Utilities.isString(n.metaData['jsClickCallback']) && n.metaData['jsClickCallback'] !== '') { //this is a legacy tree node! var jsPrefix = 'javascript:'; var js; @@ -460,6 +471,9 @@ var queryParams = {}; if ($scope.selectedLanguage && $scope.selectedLanguage.culture) { queryParams['culture'] = $scope.selectedLanguage.culture; + if (!mainCulture) { + $location.search('mculture', $scope.selectedLanguage.culture); + } } var queryString = $.param(queryParams); //create the query string from the params object } @@ -608,7 +622,7 @@ //} //execute them sequentially // set selected language to active - angular.forEach($scope.languages, function (language) { + Utilities.forEach($scope.languages, function (language) { language.active = false; }); language.active = true; @@ -702,30 +716,48 @@ * Controls the dashboards of the application * */ - function DashboardController($scope, $routeParams, dashboardResource, localizationService) { + function DashboardController($scope, $q, $routeParams, $location, dashboardResource, localizationService) { + var DASHBOARD_QUERY_PARAM = 'dashboard'; $scope.page = {}; $scope.page.nameLocked = true; $scope.page.loading = true; $scope.dashboard = {}; - localizationService.localize('sections_' + $routeParams.section).then(function (name) { + var promises = []; + promises.push(localizationService.localize('sections_' + $routeParams.section).then(function (name) { $scope.dashboard.name = name; - }); - dashboardResource.getDashboard($routeParams.section).then(function (tabs) { + })); + promises.push(dashboardResource.getDashboard($routeParams.section).then(function (tabs) { $scope.dashboard.tabs = tabs; - // set first tab to active if ($scope.dashboard.tabs && $scope.dashboard.tabs.length > 0) { - $scope.dashboard.tabs[0].active = true; + initActiveTab(); } + })); + $q.all(promises).then(function () { $scope.page.loading = false; }); $scope.changeTab = function (tab) { - $scope.dashboard.tabs.forEach(function (tab) { - tab.active = false; - }); + if ($scope.dashboard.tabs && $scope.dashboard.tabs.length > 0) { + $scope.dashboard.tabs.forEach(function (tab) { + tab.active = false; + }); + } tab.active = true; + $location.search(DASHBOARD_QUERY_PARAM, tab.alias); }; + function initActiveTab() { + // Check the query parameter for a dashboard alias + var dashboardAlias = $location.search()[DASHBOARD_QUERY_PARAM]; + var dashboardIndex = $scope.dashboard.tabs.findIndex(function (tab) { + return tab.alias === dashboardAlias; + }); + // Set the first dashboard to active if there is no query parameter or we can't find a matching dashboard for the alias + var activeIndex = dashboardIndex !== -1 ? dashboardIndex : 0; + var tab = $scope.dashboard.tabs[activeIndex]; + tab.active = true; + $location.search(DASHBOARD_QUERY_PARAM, tab.alias); + } } - //register it + // Register it angular.module('umbraco').controller('Umbraco.DashboardController', DashboardController); 'use strict'; (function () { @@ -846,8 +878,8 @@ } } function openTourGroup(tourAlias) { - angular.forEach(vm.tours, function (group) { - angular.forEach(group, function (tour) { + vm.tours.forEach(function (group) { + group.tours.forEach(function (tour) { if (tour.alias === tourAlias) { group.open = true; } @@ -856,9 +888,9 @@ } function getTourGroupCompletedPercentage() { // Finding out, how many tours are completed for the progress circle - angular.forEach(vm.tours, function (group) { + vm.tours.forEach(function (group) { var completedTours = 0; - angular.forEach(group.tours, function (tour) { + group.tours.forEach(function (tour) { if (tour.completed) { completedTours++; } @@ -902,25 +934,177 @@ angular.module('umbraco').controller('Umbraco.Drawers.Help', HelpDrawerController); }()); 'use strict'; + angular.module('umbraco').controller('Umbraco.Editors.BlockEditorController', function ($scope, localizationService, formHelper, overlayService) { + var vm = this; + vm.model = $scope.model; + vm.tabs = []; + localizationService.localizeMany([ + vm.model.createFlow ? 'general_cancel' : vm.model.liveEditing ? 'prompt_discardChanges' : 'general_close', + vm.model.createFlow ? 'general_create' : vm.model.liveEditing ? 'buttons_confirmActionConfirm' : 'buttons_submitChanges' + ]).then(function (data) { + vm.closeLabel = data[0]; + vm.submitLabel = data[1]; + }); + if (vm.model.content && vm.model.content.variants) { + var apps = vm.model.content.apps; + // configure the content app based on settings + var contentApp = apps.find(function (entry) { + return entry.alias === 'umbContent'; + }); + if (contentApp) { + if (vm.model.hideContent) { + apps.splice(apps.indexOf(contentApp), 1); + } + contentApp.active = vm.model.openSettings !== true; + } + if (vm.model.settings && vm.model.settings.variants) { + var settingsApp = apps.find(function (entry) { + return entry.alias === 'settings'; + }); + if (settingsApp) { + settingsApp.active = vm.model.openSettings === true; + } + } + vm.tabs = apps; + } + vm.submitAndClose = function () { + if (vm.model && vm.model.submit) { + // always keep server validations since this will be a nested editor and server validations are global + if (formHelper.submitForm({ + scope: $scope, + formCtrl: vm.blockForm, + keepServerValidation: true + })) { + vm.model.submit(vm.model); + vm.saveButtonState = 'success'; + } else { + vm.saveButtonState = 'error'; + } + } + }; + vm.close = function () { + if (vm.model && vm.model.close) { + // TODO: At this stage there could very well have been server errors that have been cleared + // but if we 'close' we are basically cancelling the value changes which means we'd want to cancel + // all of the server errors just cleared. It would be possible to do that but also quite annoying. + // The rudimentary way would be to: + // * Track all cleared server errors here by subscribing to the prefix validation of controls contained here + // * If this is closed, re-add all of those server validation errors + // A more robust way to do this would be to: + // * Add functionality to the serverValidationManager whereby we can remove validation errors and it will + // maintain a copy of the original errors + // * It would have a 'commit' method to commit the removed errors - which we would call in the formHelper.submitForm when it's successful + // * It would have a 'rollback' method to reset the removed errors - which we would call here + if (vm.model.createFlow === true || vm.blockForm.$dirty === true) { + var labels = vm.model.createFlow === true ? [ + 'blockEditor_confirmCancelBlockCreationHeadline', + 'blockEditor_confirmCancelBlockCreationMessage' + ] : [ + 'prompt_discardChanges', + 'blockEditor_blockHasChanges' + ]; + localizationService.localizeMany(labels).then(function (localizations) { + var confirm = { + title: localizations[0], + view: 'default', + content: localizations[1], + submitButtonLabelKey: 'general_discard', + submitButtonStyle: 'danger', + closeButtonLabelKey: 'prompt_stay', + submit: function submit() { + overlayService.close(); + vm.model.close(vm.model); + }, + close: function close() { + overlayService.close(); + } + }; + overlayService.open(confirm); + }); + } else { + vm.model.close(vm.model); + } + } + }; + }); + 'use strict'; + angular.module('umbraco').controller('Umbraco.Editors.BlockPickerController', function ($scope, localizationService) { + var vm = this; + vm.navigation = []; + vm.filter = { searchTerm: '' }; + localizationService.localizeMany([ + 'blockEditor_tabCreateEmpty', + 'blockEditor_tabClipboard' + ]).then(function (data) { + vm.navigation = [ + { + 'alias': 'empty', + 'name': data[0], + 'icon': 'icon-add', + 'view': '' + }, + { + 'alias': 'clipboard', + 'name': data[1], + 'icon': 'icon-paste-in', + 'view': '', + 'disabled': vm.model.clipboardItems.length === 0 + } + ]; + if (vm.model.openClipboard === true) { + vm.activeTab = vm.navigation[1]; + } else { + vm.activeTab = vm.navigation[0]; + } + vm.activeTab.active = true; + }); + vm.onNavigationChanged = function (tab) { + vm.activeTab.active = false; + vm.activeTab = tab; + vm.activeTab.active = true; + }; + vm.clickClearClipboard = function () { + vm.onNavigationChanged(vm.navigation[0]); + vm.navigation[1].disabled = true; + // disabled ws determined when creating the navigation, so we need to update it here. + vm.model.clipboardItems = []; + // This dialog is not connected via the clipboardService events, so we need to update manually. + vm.model.clickClearClipboard(); + }; + vm.model = $scope.model; + vm.selectItem = function (item, $event) { + vm.model.selectedItem = item; + vm.model.submit($scope.model, $event); + }; + vm.close = function () { + if ($scope.model && $scope.model.close) { + $scope.model.close($scope.model); + } + }; + }); + 'use strict'; (function () { 'use strict'; - function CompositionsController($scope, $location, $filter, overlayService, localizationService) { + function CompositionsController($scope, $location, $filter, $timeout, overlayService, localizationService) { var vm = this; var oldModel = null; vm.showConfirmSubmit = false; + vm.loadingAlias = null; vm.isSelected = isSelected; vm.openContentType = openContentType; + vm.selectCompositeContentType = selectCompositeContentType; vm.submit = submit; vm.close = close; function onInit() { /* make a copy of the init model so it is possible to roll back the changes on cancel */ - oldModel = angular.copy($scope.model); + oldModel = Utilities.copy($scope.model); if (!$scope.model.title) { $scope.model.title = 'Compositions'; } - // group the content types by their container paths + // Group the content types by their container paths vm.availableGroups = $filter('orderBy')(_.map(_.groupBy($scope.model.availableCompositeContentTypes, function (compositeContentType) { + compositeContentType.selected = isSelected(compositeContentType.contentType.alias); return compositeContentType.contentType.metaData.containerPath; }), function (group) { return { @@ -935,11 +1119,26 @@ if ($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { return true; } + return false; } function openContentType(contentType, section) { var url = (section === 'documentType' ? '/settings/documenttypes/edit/' : '/settings/mediaTypes/edit/') + contentType.id; $location.path(url); } + function selectCompositeContentType(compositeContentType) { + vm.loadingAlias = compositeContentType.contentType.alias; + var contentType = compositeContentType.contentType; + $scope.model.selectCompositeContentType(contentType).then(function (response) { + vm.loadingAlias = null; + }); + // Check if the template is already selected. + var index = $scope.model.contentType.compositeContentTypes.indexOf(contentType.alias); + if (index === -1) { + $scope.model.contentType.compositeContentTypes.push(contentType.alias); + } else { + $scope.model.contentType.compositeContentTypes.splice(index, 1); + } + } function submit() { if ($scope.model && $scope.model.submit) { // check if any compositions has been removed @@ -1103,6 +1302,86 @@ 'use strict'; /** * @ngdoc controller + * @name Umbraco.Editors.DataTypeConfigurationPickerController + * @function + * + * @description + * The controller for the content type editor data type configuration picker dialog + */ + (function () { + 'use strict'; + function DataTypeConfigurationPicker($scope, $filter, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService, editorService) { + var vm = this; + vm.configs = []; + vm.loading = true; + vm.newDataType = newDataType; + vm.pickDataType = pickDataType; + vm.close = close; + function activate() { + setTitle(); + load(); + } + function setTitle() { + if (!$scope.model.title) { + localizationService.localize('defaultdialogs_selectEditorConfiguration').then(function (data) { + $scope.model.title = data; + }); + } + } + function load() { + dataTypeResource.getGroupedDataTypes().then(function (configs) { + var filteredConfigs = []; + Object.values(configs).forEach(function (configGroup) { + for (var i = 0; i < configGroup.length; i++) { + if (configGroup[i].alias === $scope.model.editor.alias) { + filteredConfigs.push(configGroup[i]); + } + } + }); + vm.configs = filteredConfigs; + vm.loading = false; + }); + } + function newDataType() { + var dataTypeSettings = { + propertyEditor: $scope.model.editor, + property: $scope.model.property, + contentTypeName: $scope.model.contentTypeName, + create: true, + view: 'views/common/infiniteeditors/datatypesettings/datatypesettings.html', + submit: function submit(model) { + contentTypeResource.getPropertyTypeScaffold(model.dataType.id).then(function (propertyType) { + $scope.model.submit(model.dataType, propertyType, true); + editorService.close(); + }); + }, + close: function close() { + editorService.close(); + } + }; + editorService.open(dataTypeSettings); + } + function pickDataType(selectedConfig) { + selectedConfig.loading = true; + dataTypeResource.getById(selectedConfig.id).then(function (dataType) { + contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function (propertyType) { + selectedConfig.loading = false; + $scope.model.submit(dataType, propertyType, false); + }); + }); + } + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + activate(); + } + angular.module('umbraco').controller('Umbraco.Editors.DataTypeConfigurationPickerController', DataTypeConfigurationPicker); + }()); + 'use strict'; + /** + * @ngdoc controller * @name Umbraco.Editors.DataTypePickerController * @function * @@ -1113,26 +1392,20 @@ 'use strict'; function DataTypePicker($scope, $filter, dataTypeResource, contentTypeResource, localizationService, editorService) { var vm = this; + vm.showDataTypes = true; + vm.dataTypes = []; + vm.loading = true; + vm.loadingConfigs = false; vm.searchTerm = ''; - vm.showTabs = false; - vm.tabsLoaded = 0; - vm.typesAndEditors = []; - vm.userConfigured = []; - vm.loading = false; - vm.tabs = []; - vm.labels = {}; - vm.onTabChange = onTabChange; - vm.filterItems = filterItems; - vm.showDetailsOverlay = showDetailsOverlay; - vm.hideDetailsOverlay = hideDetailsOverlay; - vm.pickEditor = pickEditor; + vm.searchResult = null; + vm.viewOptionsForEditor = viewOptionsForEditor; vm.pickDataType = pickDataType; + vm.pickEditor = pickEditor; vm.close = close; + vm.searchTermChanged = searchTermChanged; function activate() { setTitle(); - loadTabs(); - getGroupedDataTypes(); - getGroupedPropertyEditors(); + loadTypes(); } function setTitle() { if (!$scope.model.title) { @@ -1141,102 +1414,77 @@ }); } } - function loadTabs() { - var labels = [ - 'contentTypeEditor_availableEditors', - 'contentTypeEditor_reuse' - ]; - localizationService.localizeMany(labels).then(function (data) { - vm.labels.availableDataTypes = data[0]; - vm.labels.reuse = data[1]; - vm.tabs = [ - { - active: true, - id: 1, - label: vm.labels.availableDataTypes, - alias: 'Default', - typesAndEditors: [] - }, - { - active: false, - id: 2, - label: vm.labels.reuse, - alias: 'Reuse', - userConfigured: [] - } - ]; - }); - } - 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 loadTypes() { + dataTypeResource.getGroupedPropertyEditors().then(function (dataTypes) { + vm.dataTypes = dataTypes; + vm.loading = false; }); } - function getGroupedDataTypes() { + function loadConfigurations() { vm.loading = true; - dataTypeResource.getGroupedDataTypes().then(function (data) { - vm.tabs[1].userConfigured = data; - vm.userConfigured = data; - vm.tabsLoaded = vm.tabsLoaded + 1; - checkIfTabContentIsLoaded(); + vm.loadingConfigs = true; + dataTypeResource.getGroupedDataTypes().then(function (configs) { + vm.configs = configs; + vm.loading = false; + performeSearch(); }); } - function checkIfTabContentIsLoaded() { - if (vm.tabsLoaded === 2) { - vm.loading = false; - vm.showTabs = true; + function searchTermChanged() { + vm.showDataTypes = vm.searchTerm === ''; + if (vm.loadingConfigs !== true) { + loadConfigurations(); + } else { + performeSearch(); } } - function onTabChange(selectedTab) { - vm.tabs.forEach(function (tab) { - tab.active = false; - }); - selectedTab.active = true; - } - function filterItems() { - // clear item details - $scope.model.itemDetails = null; + function performeSearch() { if (vm.searchTerm) { - vm.showTabs = false; - var regex = new RegExp(vm.searchTerm, 'i'); - var userConfigured = filterCollection(vm.userConfigured, regex), typesAndEditors = filterCollection(vm.typesAndEditors, regex); - var totalResults = _.reduce(_.pluck(_.union(userConfigured, typesAndEditors), 'count'), function (m, n) { - return m + n; - }, 0); - vm.filterResult = { - userConfigured: userConfigured, - typesAndEditors: typesAndEditors, - totalResults: totalResults - }; + if (vm.configs) { + var regex = new RegExp(vm.searchTerm, 'i'); + vm.searchResult = { + configs: filterCollection(vm.configs, regex), + dataTypes: filterCollection(vm.dataTypes, regex) + }; + } } else { - vm.filterResult = null; - vm.showTabs = true; + vm.searchResult = null; } } function filterCollection(collection, regex) { return _.map(_.keys(collection), function (key) { - var filteredDataTypes = $filter('filter')(collection[key], function (dataType) { - return regex.test(dataType.name) || regex.test(dataType.alias); - }); return { group: key, - count: filteredDataTypes.length, - dataTypes: filteredDataTypes + entries: $filter('filter')(collection[key], function (dataType) { + return regex.test(dataType.name) || regex.test(dataType.alias); + }) }; }); } - function showDetailsOverlay(property) { - var propertyDetails = {}; - propertyDetails.icon = property.icon; - propertyDetails.title = property.name; - $scope.model.itemDetails = propertyDetails; + function viewOptionsForEditor(editor) { + var dataTypeConfigurationPicker = { + editor: editor, + property: $scope.model.property, + contentTypeName: $scope.model.contentTypeName, + view: 'views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html', + size: 'small', + submit: function submit(dataType, propertyType, isNew) { + _submit(dataType, propertyType, isNew); + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.open(dataTypeConfigurationPicker); } - function hideDetailsOverlay() { - $scope.model.itemDetails = null; + function pickDataType(selectedDataType) { + selectedDataType.loading = true; + dataTypeResource.getById(selectedDataType.id).then(function (dataType) { + contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function (propertyType) { + selectedDataType.loading = false; + _submit(dataType, propertyType, false); + }); + }); } function pickEditor(propertyEditor) { var dataTypeSettings = { @@ -1257,15 +1505,6 @@ }; editorService.open(dataTypeSettings); } - function pickDataType(selectedDataType) { - selectedDataType.loading = true; - dataTypeResource.getById(selectedDataType.id).then(function (dataType) { - contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function (propertyType) { - selectedDataType.loading = false; - _submit(dataType, propertyType, false); - }); - }); - } function _submit(dataType, propertyType, isNew) { // update property $scope.model.property.config = propertyType.config; @@ -1342,7 +1581,7 @@ newDataType.name = nameArray.join(' - '); // get pre values dataTypeResource.getPreValues(newDataType.selectedEditor).then(function (preValues) { - newDataType.preValues = preValues; + newDataType.preValues = dataTypeHelper.createPreValueProps(preValues); vm.dataType = newDataType; vm.loadingDataType = false; }); @@ -1351,6 +1590,7 @@ function getDataType() { vm.loadingDataType = true; dataTypeResource.getById($scope.model.id).then(function (dataType) { + dataType.preValues = dataTypeHelper.createPreValueProps(dataType.preValues); vm.dataType = dataType; vm.loadingDataType = false; }); @@ -1407,10 +1647,13 @@ preview: '', success: false, info: '', - supportsDimensions: false + a11yInfo: '', + supportsDimensions: false, + originalWidth: 360, + originalHeight: 240 }; if ($scope.model.modify) { - angular.extend($scope.model.embed, $scope.model.modify); + Utilities.extend($scope.model.embed, $scope.model.modify); showPreview(); } vm.toggleConstrain = toggleConstrain; @@ -1429,6 +1672,7 @@ if ($scope.model.embed.url) { $scope.model.embed.show = true; $scope.model.embed.info = ''; + $scope.model.embed.a11yInfo = ''; $scope.model.embed.success = false; vm.loading = true; $http({ @@ -1446,6 +1690,7 @@ //not supported $scope.model.embed.preview = ''; $scope.model.embed.info = 'Not supported'; + $scope.model.embed.a11yInfo = $scope.model.embed.info; $scope.model.embed.success = false; $scope.model.embed.supportsDimensions = false; vm.trustedPreview = null; @@ -1454,6 +1699,7 @@ //error $scope.model.embed.preview = ''; $scope.model.embed.info = 'Could not embed media - please ensure the URL is valid'; + $scope.model.embed.a11yInfo = $scope.model.embed.info; $scope.model.embed.success = false; $scope.model.embed.supportsDimensions = false; vm.trustedPreview = null; @@ -1462,6 +1708,8 @@ $scope.model.embed.success = true; $scope.model.embed.supportsDimensions = response.data.SupportsDimensions; $scope.model.embed.preview = response.data.Markup; + $scope.model.embed.info = ''; + $scope.model.embed.a11yInfo = 'Retrieved URL'; vm.trustedPreview = $sce.trustAsHtml(response.data.Markup); break; } @@ -1471,20 +1719,24 @@ $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ''; $scope.model.embed.info = 'Could not embed media - please ensure the URL is valid'; + $scope.model.embed.a11yInfo = $scope.model.embed.info; vm.loading = false; }); } else { $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ''; $scope.model.embed.info = 'Please enter a URL'; + $scope.model.embed.a11yInfo = $scope.model.embed.info; } } function changeSize(type) { - var width, height; + var width = parseInt($scope.model.embed.width, 10); + var height = parseInt($scope.model.embed.height, 10); + var originalWidth = parseInt($scope.model.embed.originalWidth, 10); + var originalHeight = parseInt($scope.model.embed.originalHeight, 10); + var resize = originalWidth !== width || originalHeight !== height; if ($scope.model.embed.constrain) { - width = parseInt($scope.model.embed.width, 10); - height = parseInt($scope.model.embed.height, 10); - if (type == 'width') { + if (type === 'width') { origHeight = Math.round(width / origWidth * height); $scope.model.embed.height = origHeight; } else { @@ -1492,7 +1744,9 @@ $scope.model.embed.width = origWidth; } } - if ($scope.model.embed.url !== '') { + $scope.model.embed.originalWidth = $scope.model.embed.width; + $scope.model.embed.originalHeight = $scope.model.embed.height; + if ($scope.model.embed.url !== '' && resize) { showPreview(); } } @@ -1522,8 +1776,9 @@ * @description * The controller for the content type editor icon picker */ - function IconPickerController($scope, iconHelper, localizationService) { + function IconPickerController($scope, localizationService, iconHelper) { var vm = this; + vm.filter = { searchTerm: '' }; vm.selectIcon = selectIcon; vm.selectColor = selectColor; vm.submit = submit; @@ -1531,7 +1786,8 @@ vm.colors = [ { name: 'Black', - value: 'color-black' + value: 'color-black', + default: true }, { name: 'Blue Grey', @@ -1609,12 +1865,30 @@ function onInit() { vm.loading = true; setTitle(); - iconHelper.getIcons().then(function (icons) { + iconHelper.getAllIcons().then(function (icons) { vm.icons = icons; - vm.loading = false; + // Get's legacy icons, removes duplicates then maps them to IconModel + iconHelper.getIcons().then(function (icons) { + if (icons && icons.length > 0) { + var legacyIcons = icons.filter(function (icon) { + return !vm.icons.find(function (x) { + return x.name == icon; + }); + }).map(function (icon) { + return { + name: icon, + svgString: null + }; + }); + vm.icons = legacyIcons.concat(vm.icons); + } + vm.loading = false; + }); }); // set a default color if nothing is passed in - vm.color = $scope.model.color ? findColor($scope.model.color) : vm.colors[0]; + vm.color = $scope.model.color ? findColor($scope.model.color) : vm.colors.find(function (x) { + return x.default; + }); // if an icon is passed in - preselect it vm.icon = $scope.model.icon ? $scope.model.icon : undefined; } @@ -1631,11 +1905,16 @@ submit(); } function findColor(value) { - return _.findWhere(vm.colors, { value: value }); + return vm.colors.find(function (x) { + return x.value === value; + }); } - function selectColor(color, $index, $event) { - $scope.model.color = color.value; - vm.color = color; + function selectColor(color) { + var newColor = color || vm.colors.find(function (x) { + return x.default; + }); + $scope.model.color = newColor.value; + vm.color = newColor; } function close() { if ($scope.model && $scope.model.close) { @@ -1925,7 +2204,7 @@ }; if (dialogOptions.currentTarget) { // clone the current target so we don't accidentally update the caller's model while manipulating $scope.model.target - $scope.model.target = angular.copy(dialogOptions.currentTarget); + $scope.model.target = Utilities.copy(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 @@ -1997,7 +2276,7 @@ $scope.model.target.url = resp.url; }); } - if (!angular.isUndefined($scope.model.target.isMedia)) { + if (!Utilities.isUndefined($scope.model.target.isMedia)) { delete $scope.model.target.isMedia; } } @@ -2172,8 +2451,6 @@ $scope.model.itemDetails = null; } function pickParameterEditor(selectedParameterEditor) { - console.log('pickParameterEditor', selectedParameterEditor); - console.log('$scope.model', $scope.model); $scope.model.parameter.editor = selectedParameterEditor.alias; $scope.model.parameter.dataTypeName = selectedParameterEditor.name; $scope.model.parameter.dataTypeIcon = selectedParameterEditor.icon; @@ -2191,10 +2468,13 @@ 'use strict'; function MacroPickerController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper, localizationService) { $scope.macros = []; + $scope.a11yInfo = ''; $scope.model.selectedMacro = null; $scope.model.macroParams = []; + $scope.displayA11YMessageForFilter = displayA11YMessageForFilter; $scope.wizardStep = 'macroSelect'; $scope.noMacroParams = false; + $scope.model.searchTerm = ''; function onInit() { if (!$scope.model.title) { localizationService.localize('defaultdialogs_selectMacro').then(function (value) { @@ -2207,6 +2487,7 @@ if ($scope.wizardStep === 'macroSelect') { editParams(true); } else { + $scope.$broadcast('formSubmitting', { scope: $scope }); $scope.model.submit($scope.model); } }; @@ -2223,11 +2504,12 @@ //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) { + if (!Utilities.isArray(data) || data.length === 0) { if (insertIfNoParameters) { $scope.model.submit($scope.model); } else { $scope.wizardStep = 'macroSelect'; + displayA11yMessages($scope.macros); } } else { $scope.wizardStep = 'paramSelect'; @@ -2245,8 +2527,8 @@ //detect if it is a json string if (val.detectIsJson()) { try { - //Parse it to json - prop.value = angular.fromJson(val); + //Parse it from json + prop.value = Utilities.fromJson(val); } catch (e) { // not json prop.value = val; @@ -2263,6 +2545,29 @@ } }); } + function displayA11yMessages(macros) { + if ($scope.noMacroParams || !macros || macros.length === 0) + localizationService.localize('general_searchNoResult').then(function (value) { + $scope.a11yInfo = value; + }); + else if (macros) { + if (macros.length === 1) { + localizationService.localize('treeSearch_searchResult').then(function (value) { + $scope.a11yInfo = '1 ' + value; + }); + } else { + localizationService.localize('treeSearch_searchResults').then(function (value) { + $scope.a11yInfo = macros.length + ' ' + value; + }); + } + } + } + function displayA11YMessageForFilter() { + var macros = _.filter($scope.macros, function (v) { + return v.name.toLowerCase().includes($scope.model.searchTerm.toLowerCase()); + }); + displayA11yMessages(macros); + } //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) { @@ -2270,11 +2575,11 @@ } //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) { + if (Utilities.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) { + if (Utilities.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); }); @@ -2301,52 +2606,222 @@ //we don't have a pre-selected macro so ensure the correct step is set $scope.wizardStep = 'macroSelect'; } + displayA11yMessages($scope.macros); }); onInit(); } angular.module('umbraco').controller('Umbraco.Overlays.MacroPickerController', MacroPickerController); 'use strict'; - //used for the media picker dialog - angular.module('umbraco').controller('Umbraco.Editors.MediaPickerController', function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService) { + angular.module('umbraco').controller('Umbraco.Editors.MediaEntryEditorController', function ($scope, localizationService, entityResource, editorService, overlayService, eventsService, mediaHelper) { + var unsubscribe = []; var vm = this; - vm.submit = submit; - vm.close = _close; - vm.toggle = toggle; - vm.upload = upload; - vm.dragLeave = dragLeave; - vm.dragEnter = dragEnter; - vm.onUploadComplete = onUploadComplete; - vm.onFilesQueue = onFilesQueue; - vm.changeSearch = changeSearch; - vm.submitFolder = submitFolder; - vm.enterSubmitFolder = enterSubmitFolder; - vm.focalPointChanged = focalPointChanged; - vm.changePagination = changePagination; - vm.clickHandler = clickHandler; - vm.clickItemName = clickItemName; - vm.gotoFolder = gotoFolder; - var dialogOptions = $scope.model; - $scope.disableFolderSelect = dialogOptions.disableFolderSelect && dialogOptions.disableFolderSelect !== '0' ? true : false; - $scope.disableFocalPoint = dialogOptions.disableFocalPoint && dialogOptions.disableFocalPoint !== '0' ? true : false; - $scope.onlyImages = dialogOptions.onlyImages && dialogOptions.onlyImages !== '0' ? true : false; - $scope.onlyFolders = dialogOptions.onlyFolders && dialogOptions.onlyFolders !== '0' ? true : false; - $scope.showDetails = dialogOptions.showDetails && dialogOptions.showDetails !== '0' ? true : false; - $scope.multiPicker = dialogOptions.multiPicker && dialogOptions.multiPicker !== '0' ? true : false; - $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; - $scope.cropSize = dialogOptions.cropSize; + vm.loading = true; + vm.model = $scope.model; + vm.mediaEntry = vm.model.mediaEntry; + vm.currentCrop = null; + localizationService.localizeMany([ + vm.model.createFlow ? 'general_cancel' : 'general_close', + vm.model.createFlow ? 'general_create' : 'buttons_submitChanges' + ]).then(function (data) { + vm.closeLabel = data[0]; + vm.submitLabel = data[1]; + }); + vm.title = ''; + function init() { + updateMedia(); + unsubscribe.push(eventsService.on('editors.media.saved', function (name, args) { + // if this media item uses the updated media type we want to reload the media file + if (args && args.media && args.media.key === vm.mediaEntry.mediaKey) { + updateMedia(); + } + })); + } + function updateMedia() { + vm.loading = true; + entityResource.getById(vm.mediaEntry.mediaKey, 'Media').then(function (mediaEntity) { + vm.media = mediaEntity; + vm.imageSrc = mediaHelper.resolveFileFromEntity(mediaEntity, true); + vm.loading = false; + vm.hasDimensions = false; + vm.isCroppable = false; + localizationService.localize('mediaPicker_editMediaEntryLabel', [ + vm.media.name, + vm.model.documentName + ]).then(function (data) { + vm.title = data; + }); + }, function () { + localizationService.localize('mediaPicker_deletedItem').then(function (localized) { + vm.media = { + name: localized, + icon: 'icon-picture', + trashed: true + }; + vm.loading = false; + vm.hasDimensions = false; + vm.isCroppable = false; + }); + }); + } + vm.onImageLoaded = onImageLoaded; + function onImageLoaded(isCroppable, hasDimensions) { + vm.isCroppable = isCroppable; + vm.hasDimensions = hasDimensions; + } + ; + vm.repickMedia = repickMedia; + function repickMedia() { + vm.model.propertyEditor.changeMediaFor(vm.model.mediaEntry, onMediaReplaced); + } + function onMediaReplaced() { + // mark we have changes: + vm.imageCropperForm.$setDirty(); + // un-select crop: + vm.currentCrop = null; + // + updateMedia(); + } + vm.openMedia = openMedia; + function openMedia() { + var mediaEditor = { + id: vm.mediaEntry.mediaKey, + submit: function submit() { + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.mediaEditor(mediaEditor); + } + vm.focalPointChanged = function (left, top) { + //update the model focalpoint value + vm.mediaEntry.focalPoint = { + left: left, + top: top + }; + //set form to dirty to track changes + setDirty(); + }; + vm.selectCrop = selectCrop; + function selectCrop(targetCrop) { + vm.currentCrop = targetCrop; + setDirty(); // TODO: start watchin values of crop, first when changed set to dirty. + } + ; + vm.deselectCrop = deselectCrop; + function deselectCrop() { + vm.currentCrop = null; + } + ; + vm.resetCrop = resetCrop; + function resetCrop() { + if (vm.currentCrop) { + $scope.$evalAsync(function () { + vm.model.propertyEditor.resetCrop(vm.currentCrop); + vm.forceUpdateCrop = Math.random(); + }); + } + } + function setDirty() { + vm.imageCropperForm.$setDirty(); + } + vm.submitAndClose = function () { + if (vm.model && vm.model.submit) { + vm.model.submit(vm.model); + } + }; + vm.close = function () { + if (vm.model && vm.model.close) { + if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true) { + var labels = vm.model.createFlow === true ? [ + 'mediaPicker_confirmCancelMediaEntryCreationHeadline', + 'mediaPicker_confirmCancelMediaEntryCreationMessage' + ] : [ + 'prompt_discardChanges', + 'mediaPicker_confirmCancelMediaEntryHasChanges' + ]; + localizationService.localizeMany(labels).then(function (localizations) { + var confirm = { + title: localizations[0], + view: 'default', + content: localizations[1], + submitButtonLabelKey: 'general_discard', + submitButtonStyle: 'danger', + closeButtonLabelKey: 'prompt_stay', + submit: function submit() { + overlayService.close(); + vm.model.close(vm.model); + }, + close: function close() { + overlayService.close(); + } + }; + overlayService.open(confirm); + }); + } else { + vm.model.close(vm.model); + } + } + }; + init(); + $scope.$on('$destroy', function () { + unsubscribe.forEach(function (x) { + return x(); + }); + }); + }); + 'use strict'; + //used for the media picker dialog + angular.module('umbraco').controller('Umbraco.Editors.MediaPickerController', function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage, notificationsService, clipboardService) { + var vm = this; + vm.submit = submit; + vm.close = close; + vm.toggle = toggle; + vm.upload = upload; + vm.dragLeave = dragLeave; + vm.dragEnter = dragEnter; + vm.onUploadComplete = onUploadComplete; + vm.onFilesQueue = onFilesQueue; + vm.changeSearch = changeSearch; + vm.submitFolder = submitFolder; + vm.enterSubmitFolder = enterSubmitFolder; + vm.focalPointChanged = focalPointChanged; + vm.changePagination = changePagination; + vm.onNavigationChanged = onNavigationChanged; + vm.clickClearClipboard = clickClearClipboard; + vm.clickHandler = clickHandler; + vm.clickItemName = clickItemName; + vm.gotoFolder = gotoFolder; + vm.toggleListView = toggleListView; + vm.selectLayout = selectLayout; + vm.showMediaList = false; + vm.navigation = []; + var dialogOptions = $scope.model; + vm.clipboardItems = dialogOptions.clipboardItems; + $scope.disableFolderSelect = dialogOptions.disableFolderSelect && dialogOptions.disableFolderSelect !== '0' ? true : false; + $scope.disableFocalPoint = dialogOptions.disableFocalPoint && dialogOptions.disableFocalPoint !== '0' ? true : false; + $scope.onlyImages = dialogOptions.onlyImages && dialogOptions.onlyImages !== '0' ? true : false; + $scope.onlyFolders = dialogOptions.onlyFolders && dialogOptions.onlyFolders !== '0' ? true : false; + $scope.showDetails = dialogOptions.showDetails && dialogOptions.showDetails !== '0' ? true : false; + $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; + $scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false; + $scope.filterOptions = { excludeSubFolders: umbSessionStorage.get('mediaPickerExcludeSubFolders') || false }; var userStartNodes = []; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); if ($scope.onlyImages) { vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); } else { - // Use whitelist of allowed file types if provided + // Use list of allowed file types if provided if (allowedUploadFiles !== '') { vm.acceptedFileTypes = allowedUploadFiles; } else { - // If no whitelist, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + // If no allowed list, we pass in a disallowed list by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); } } @@ -2368,6 +2843,28 @@ filter: '', dataTypeKey: dataTypeKey }; + vm.layout = { + layouts: [ + { + name: 'Grid', + icon: 'icon-thumbnails-small', + path: 'gridpath', + selected: true + }, + { + name: 'List', + icon: 'icon-list', + path: 'listpath', + selected: true + } + ], + activeLayout: { + name: 'Grid', + icon: 'icon-thumbnails-small', + path: 'gridpath', + selected: true + } + }; // preload selected item $scope.target = null; if (dialogOptions.currentTarget) { @@ -2375,8 +2872,28 @@ } function setTitle() { if (!$scope.model.title) { - localizationService.localize('defaultdialogs_selectMedia').then(function (data) { - $scope.model.title = data; + localizationService.localizeMany([ + 'defaultdialogs_selectMedia', + 'mediaPicker_tabClipboard' + ]).then(function (data) { + $scope.model.title = data[0]; + vm.navigation = [{ + 'alias': 'empty', + 'name': data[0], + 'icon': 'icon-umb-media', + 'active': true, + 'view': '' + }]; + if (vm.clipboardItems) { + vm.navigation.push({ + 'alias': 'clipboard', + 'name': data[1], + 'icon': 'icon-paste-in', + 'view': '', + 'disabled': vm.clipboardItems.length === 0 + }); + } + vm.activeTab = vm.navigation[0]; }); } } @@ -2405,20 +2922,26 @@ } 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 originalTarget = $scope.target; var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; var altText = $scope.target.altText; + var caption = $scope.target.caption; // ID of a UDI or legacy int ID still could be null/undefinied here // As user may dragged in an image that has not been saved to media section yet if (id) { entityResource.getById(id, 'Media').then(function (node) { $scope.target = node; - if (ensureWithinStartNode(node)) { + // Moving directly to existing node's folder + gotoFolder({ id: node.parentId }).then(function () { selectMedia(node); $scope.target.url = mediaHelper.resolveFileFromEntity(node); $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); $scope.target.altText = altText; + $scope.target.caption = caption; + $scope.target.focalPoint = originalTarget.focalPoint; + $scope.target.coordinates = originalTarget.coordinates; openDetailsDialog(); - } + }); }, gotoStartNode); } else { // No ID set - then this is going to be a tmpimg that has not been uploaded @@ -2428,12 +2951,19 @@ } } function upload(v) { - angular.element('.umb-file-dropzone .file-select').trigger('click'); + var fileSelect = $('.umb-file-dropzone .file-select'); + if (fileSelect.length === 0) { + localizationService.localize('media_uploadNotAllowed').then(function (message) { + notificationsService.warning(message); + }); + } else { + fileSelect.trigger('click'); + } } - function dragLeave(el, event) { + function dragLeave() { $scope.activeDrag = false; } - function dragEnter(el, event) { + function dragEnter() { $scope.activeDrag = true; } function submitFolder() { @@ -2478,17 +3008,31 @@ return f.path.indexOf($scope.startNodeId) !== -1; }); }); - mediaTypeHelper.getAllowedImagetypes(folder.id).then(function (types) { - vm.acceptedMediatypes = types; - }); } else { $scope.path = []; } + mediaTypeHelper.getAllowedImagetypes(folder.id).then(function (types) { + vm.acceptedMediatypes = types; + }); $scope.lockedFolder = folder.id === -1 && $scope.model.startNodeIsVirtual || hasFolderAccess(folder) === false; $scope.currentFolder = folder; localStorageService.set('umbLastOpenedMediaNodeId', folder.id); return getChildren(folder.id); } + function toggleListView() { + vm.showMediaList = !vm.showMediaList; + } + function selectLayout(layout) { + //this somehow doesn't set the 'active=true' property for the chosen layout + vm.layout.activeLayout = layout; + //workaround + vm.layout.layouts.forEach(function (element) { + return element.active = false; + }); + layout.active = true; + //set whether to toggle the list + vm.showMediaList = layout.name === 'List'; + } function clickHandler(media, event, index) { if (media.isFolder) { if ($scope.disableFolderSelect) { @@ -2515,7 +3059,7 @@ if (item.isFolder) { gotoFolder(item); } else { - $scope.clickHandler(item, event, index); + clickHandler(item, event, index); } } ; @@ -2551,12 +3095,12 @@ gotoFolder($scope.currentFolder).then(function () { $timeout(function () { if ($scope.multiPicker) { - var images = _.rest($scope.images, $scope.images.length - files.length); - _.each(images, function (image) { - selectMedia(image); + var images = _.rest(_.sortBy($scope.images, 'id'), $scope.images.length - files.length); + images.forEach(function (image) { + return selectMedia(image); }); } else { - var image = $scope.images[$scope.images.length - 1]; + var image = _.sortBy($scope.images, 'id')[$scope.images.length - 1]; clickHandler(image); } }); @@ -2594,7 +3138,7 @@ } return false; } - function gotoStartNode(err) { + function gotoStartNode() { gotoFolder({ id: $scope.startNodeId, name: 'Media', @@ -2602,26 +3146,39 @@ }); } function openDetailsDialog() { - localizationService.localize('defaultdialogs_editSelectedMedia').then(function (data) { - vm.mediaPickerDetailsOverlay = { - show: true, - title: data, - disableFocalPoint: $scope.disableFocalPoint, - submit: function submit(model) { - $scope.model.selection.push($scope.target); - $scope.model.submit($scope.model); - vm.mediaPickerDetailsOverlay.show = false; - vm.mediaPickerDetailsOverlay = null; - }, - close: function close(oldModel) { - vm.mediaPickerDetailsOverlay.show = false; - vm.mediaPickerDetailsOverlay = null; - _close(); - } - }; + var dialog = { + size: 'small', + cropSize: $scope.cropSize, + target: $scope.target, + disableFocalPoint: $scope.disableFocalPoint, + submit: function submit() { + $scope.model.selection.push($scope.target); + $scope.model.submit($scope.model); + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + localizationService.localize('defaultdialogs_editSelectedMedia').then(function (value) { + dialog.title = value; + editorService.mediaCropDetails(dialog); }); } ; + function onNavigationChanged(tab) { + vm.activeTab.active = false; + vm.activeTab = tab; + vm.activeTab.active = true; + } + ; + function clickClearClipboard() { + vm.onNavigationChanged(vm.navigation[0]); + vm.navigation[1].disabled = true; + vm.clipboardItems = []; + dialogOptions.clickClearClipboard(); + } + ; var debounceSearchMedia = _.debounce(function () { $scope.$apply(function () { if (vm.searchOptions.filter) { @@ -2645,6 +3202,7 @@ debounceSearchMedia(); } function toggle() { + umbSessionStorage.set('mediaPickerExcludeSubFolders', $scope.filterOptions.excludeSubFolders); // Make sure to activate the changeSearch function everytime the toggle is clicked changeSearch(); } @@ -2656,11 +3214,15 @@ ; function searchMedia() { vm.loading = true; - entityResource.getPagedDescendants($scope.currentFolder.id, 'Media', vm.searchOptions).then(function (data) { + entityResource.getPagedDescendants($scope.filterOptions.excludeSubFolders ? $scope.currentFolder.id : $scope.startNodeId, 'Media', vm.searchOptions).then(function (data) { // update image data to work with image grid - angular.forEach(data.items, function (mediaItem) { - setMediaMetaData(mediaItem); - }); + if (data.items) { + var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(',') : null; + data.items.forEach(function (mediaItem) { + setMediaMetaData(mediaItem); + mediaItem.filtered = allowedTypes && allowedTypes.indexOf(mediaItem.metaData.ContentTypeAlias) < 0; + }); + } // update images $scope.images = data.items ? data.items : []; // update pagination @@ -2701,6 +3263,9 @@ value: mediaItem.metaData.umbracoFile.Value }); } + if (mediaItem.metaData.UpdateDate !== null) { + mediaItem.updateDate = mediaItem.metaData.UpdateDate; + } } } function getChildren(id) { @@ -2708,10 +3273,7 @@ return entityResource.getChildren(id, 'Media', vm.searchOptions).then(function (data) { var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(',') : null; for (var i = 0; i < data.length; i++) { - if (data[i].metaData.MediaPath !== null) { - data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); - data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); - } + setDefaultData(data[i]); data[i].filtered = allowedTypes && allowedTypes.indexOf(data[i].metaData.ContentTypeAlias) < 0; } vm.searchOptions.filter = ''; @@ -2721,11 +3283,20 @@ vm.loading = false; }); } + function setDefaultData(item) { + if (item.metaData.MediaPath !== null) { + item.thumbnail = mediaHelper.resolveFileFromEntity(item, true); + item.image = mediaHelper.resolveFileFromEntity(item, false); + } + if (item.metaData.UpdateDate !== null) { + item.updateDate = item.metaData.UpdateDate; + } + } function preSelectMedia() { for (var folderIndex = 0; folderIndex < $scope.images.length; folderIndex++) { var folderImage = $scope.images[folderIndex]; var imageIsSelected = false; - if ($scope.model && angular.isArray($scope.model.selection)) { + if ($scope.model && Utilities.isArray($scope.model.selection)) { for (var selectedIndex = 0; selectedIndex < $scope.model.selection.length; selectedIndex++) { var selectedImage = $scope.model.selection[selectedIndex]; if (folderImage.key === selectedImage.key) { @@ -2750,23 +3321,71 @@ top: top }; } - function setUpdatedMediaNodes(item) { - // add udi to list of updated media items so we easily can update them in other editors - if ($scope.model.updatedMediaNodes.indexOf(item.udi) === -1) { - $scope.model.updatedMediaNodes.push(item.udi); + function submit() { + if ($scope.model && $scope.model.submit) { + $scope.model.submit($scope.model); + } + } + function close() { + if ($scope.model && $scope.model.close) { + $scope.model.close($scope.model); } } + onInit(); + }); + 'use strict'; + angular.module('umbraco').controller('Umbraco.Editors.MediaCropDetailsController', function ($scope) { + var vm = this; + vm.submit = submit; + vm.close = close; + vm.hasCrops = cropSet() === true; + vm.focalPointChanged = focalPointChanged; + vm.disableFocalPoint = false; + if (typeof $scope.model.disableFocalPoint === 'boolean') { + vm.disableFocalPoint = $scope.model.disableFocalPoint; + } else { + vm.disableFocalPoint = $scope.model.disableFocalPoint !== undefined && $scope.model.disableFocalPoint !== '0' ? true : false; + } + if (!$scope.model.target.coordinates && !$scope.model.target.focalPoint) { + $scope.model.target.focalPoint = { + left: 0.5, + top: 0.5 + }; + } + if (!$scope.model.target.image) { + $scope.model.target.image = $scope.model.target.url; + } + if (!$scope.model.target || $scope.model.target.id || $scope.model.target.url && $scope.model.target.url.toLowerCase().startsWith('blob:')) { + vm.shouldShowUrl = false; + } else { + vm.shouldShowUrl = true; + } + /** + * Called when the umbImageGravity component updates the focal point value + * @param {any} left + * @param {any} top + */ + function focalPointChanged(left, top) { + // update the model focalpoint value + $scope.model.target.focalPoint = { + left: left, + top: top + }; + } function submit() { if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); } } - function _close() { + function close() { if ($scope.model && $scope.model.close) { $scope.model.close($scope.model); } } - onInit(); + function cropSet() { + var model = $scope.model; + return (model.cropSize || {}).width !== undefined && (model.cropSize || {}).height !== undefined; + } }); 'use strict'; //used for the member picker dialog @@ -2992,15 +3611,17 @@ vm.submit = submit; vm.close = close; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.toggleAllowSegmentVariants = toggleAllowSegmentVariants; vm.toggleValidation = toggleValidation; vm.toggleShowOnMemberProfile = toggleShowOnMemberProfile; vm.toggleMemberCanEdit = toggleMemberCanEdit; vm.toggleIsSensitiveData = toggleIsSensitiveData; + vm.toggleLabelOnTop = toggleLabelOnTop; function onInit() { userService.getCurrentUser().then(function (user) { vm.showSensitiveData = user.userGroups.indexOf('sensitiveData') != -1; }); - //make the default the same as the content type + //make the default the same as the content type if (!$scope.model.property.dataTypeId) { $scope.model.property.allowCultureVariant = $scope.model.contentTypeAllowCultureVariant; } @@ -3011,13 +3632,17 @@ 'validation_validateAsEmail', 'validation_validateAsNumber', 'validation_validateAsUrl', - 'validation_enterCustomValidation' + 'validation_enterCustomValidation', + 'validation_fieldIsMandatory', + 'contentTypeEditor_displaySettingsLabelOnTop' ]; localizationService.localizeMany(labels).then(function (data) { vm.labels.validateAsEmail = data[0]; vm.labels.validateAsNumber = data[1]; vm.labels.validateAsUrl = data[2]; vm.labels.customValidation = data[3]; + vm.labels.fieldIsMandatory = data[4]; + vm.labels.displaySettingsLabelOnTop = data[5]; vm.validationTypes = [ { 'name': vm.labels.validateAsEmail, @@ -3056,7 +3681,7 @@ property: $scope.model.property, contentTypeName: $scope.model.contentTypeName, view: 'views/common/infiniteeditors/datatypepicker/datatypepicker.html', - size: 'small', + size: 'medium', submit: function submit(model) { $scope.model.updateSameDataTypes = model.updateSameDataTypes; vm.focusOnMandatoryField = true; @@ -3122,7 +3747,7 @@ 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) { + vm.validationTypes.forEach(function (validationType, index) { if ($scope.model.property.validation.pattern === validationType.pattern) { vm.selectedValidationType = vm.validationTypes[index]; vm.showValidationPattern = true; @@ -3131,7 +3756,7 @@ }); // if there is no match - choose the custom validation option. if (!match) { - angular.forEach(vm.validationTypes, function (validationType) { + vm.validationTypes.forEach(function (validationType) { if (validationType.key === 'custom') { vm.selectedValidationType = validationType; vm.showValidationPattern = true; @@ -3159,6 +3784,9 @@ function toggleAllowCultureVariants() { $scope.model.property.allowCultureVariant = toggleValue($scope.model.property.allowCultureVariant); } + function toggleAllowSegmentVariants() { + $scope.model.property.allowSegmentVariant = toggleValue($scope.model.property.allowSegmentVariant); + } function toggleValidation() { $scope.model.property.validation.mandatory = toggleValue($scope.model.property.validation.mandatory); } @@ -3171,6 +3799,9 @@ function toggleIsSensitiveData() { $scope.model.property.isSensitiveData = toggleValue($scope.model.property.isSensitiveData); } + function toggleLabelOnTop() { + $scope.model.property.labelOnTop = toggleValue($scope.model.property.labelOnTop); + } onInit(); } angular.module('umbraco').controller('Umbraco.Editors.PropertySettingsController', PropertySettingsEditor); @@ -3416,12 +4047,16 @@ } function changeVersion(version) { if (version && version.versionId) { + vm.loading = true; var culture = $scope.model.node.variants.length > 1 ? vm.currentVersion.language.culture : null; contentResource.getRollbackVersion(version.versionId, culture).then(function (data) { vm.previousVersion = data; vm.previousVersion.versionId = version.versionId; createDiff(vm.currentVersion, vm.previousVersion); + vm.loading = false; vm.rollbackButtonDisabled = false; + }, function () { + vm.loading = false; }); } else { vm.diff = null; @@ -3451,28 +4086,47 @@ // find diff in name vm.diff.name = JsDiff.diffWords(currentVersion.name, previousVersion.name); // extract all properties from the tabs and create new object for the diff - currentVersion.tabs.forEach(function (tab, tabIndex) { - tab.properties.forEach(function (property, propertyIndex) { - var oldProperty = previousVersion.tabs[tabIndex].properties[propertyIndex]; - // we have to make properties storing values as object into strings (Grid, nested content, etc.) - if (property.value instanceof Object) { - property.value = JSON.stringify(property.value, null, 1); - property.isObject = true; + currentVersion.tabs.forEach(function (tab) { + tab.properties.forEach(function (property) { + var oldTabIndex = -1; + var oldTabPropertyIndex = -1; + var previousVersionTabs = previousVersion.tabs; + // find the property by alias, but only search until we find it + for (var oti = 0, length = previousVersionTabs.length; oti < length; oti++) { + var opi = previousVersionTabs[oti].properties.findIndex(function (p) { + return p.alias === property.alias; + }); + if (opi !== -1) { + oldTabIndex = oti; + oldTabPropertyIndex = opi; + break; + } } - if (oldProperty.value instanceof Object) { - oldProperty.value = JSON.stringify(oldProperty.value, null, 1); - oldProperty.isObject = true; + if (oldTabIndex !== -1 && oldTabPropertyIndex !== -1) { + var oldProperty = previousVersion.tabs[oldTabIndex].properties[oldTabPropertyIndex]; + // copy existing properties, so it doesn't manipulate existing properties on page + oldProperty = Utilities.copy(oldProperty); + property = Utilities.copy(property); + // we have to make properties storing values as object into strings (Grid, nested content, etc.) + if (property.value instanceof Object) { + property.value = JSON.stringify(property.value, null, 1); + property.isObject = true; + } + if (oldProperty.value instanceof Object) { + oldProperty.value = JSON.stringify(oldProperty.value, null, 1); + oldProperty.isObject = true; + } + // diff requires a string + property.value = property.value ? property.value + '' : ''; + oldProperty.value = oldProperty.value ? oldProperty.value + '' : ''; + var diffProperty = { + 'alias': property.alias, + 'label': property.label, + 'diff': property.isObject ? JsDiff.diffJson(property.value, oldProperty.value) : JsDiff.diffWords(property.value, oldProperty.value), + 'isObject': property.isObject || oldProperty.isObject + }; + vm.diff.properties.push(diffProperty); } - // diff requires a string - property.value = property.value ? property.value : ''; - oldProperty.value = oldProperty.value ? oldProperty.value : ''; - var diffProperty = { - 'alias': property.alias, - 'label': property.label, - 'diff': JsDiff.diffWords(property.value, oldProperty.value), - 'isObject': property.isObject || oldProperty.isObject ? true : false - }; - vm.diff.properties.push(diffProperty); }); }); } @@ -3536,8 +4190,8 @@ }); } function preSelect(selection) { - angular.forEach(selection, function (selected) { - angular.forEach(vm.sections, function (section) { + selection.forEach(function (selected) { + vm.sections.forEach(function (section) { if (selected.alias === section.alias) { section.selected = true; } @@ -3549,7 +4203,7 @@ section.selected = true; $scope.model.selection.push(section); } else { - angular.forEach($scope.model.selection, function (selectedSection, index) { + $scope.model.selection.forEach(function (selectedSection, index) { if (selectedSection.alias === section.alias) { section.selected = false; $scope.model.selection.splice(index, 1); @@ -3558,7 +4212,7 @@ } } function setSectionIcon(sections) { - angular.forEach(sections, function (section) { + sections.forEach(function (section) { section.icon = 'icon-section'; }); } @@ -3675,9 +4329,11 @@ vm.hideSearch = hideSearch; vm.closeMiniListView = closeMiniListView; vm.selectListViewNode = selectListViewNode; + vm.listViewItemsLoaded = listViewItemsLoaded; vm.submit = submit; vm.close = close; var currentNode = $scope.model.currentNode; + var previouslyFocusedElement = null; function initDialogTree() { vm.dialogTreeApi.callbacks.treeLoaded(treeLoadedHandler); // TODO: Also deal with unexpanding!! @@ -3688,17 +4344,19 @@ * Performs the initialization of this component */ function onInit() { - // load languages - languageResource.getAll().then(function (languages) { - vm.languages = languages; - // set the default language - vm.languages.forEach(function (language) { - if (language.isDefault) { - vm.selectedLanguage = language; - vm.languageSelectorIsOpen = false; - } + if (vm.showLanguageSelector) { + // load languages + languageResource.getAll().then(function (languages) { + vm.languages = languages; + // set the default language + vm.languages.forEach(function (language) { + if (language.isDefault) { + vm.selectedLanguage = language; + vm.languageSelectorIsOpen = false; + } + }); }); - }); + } if (vm.treeAlias === 'content') { vm.entityType = 'Document'; if (!$scope.model.title) { @@ -3765,9 +4423,9 @@ $scope.model.filterExclude = false; $scope.model.filterAdvanced = false; //used advanced filtering - if (angular.isFunction($scope.model.filter)) { + if (Utilities.isFunction($scope.model.filter)) { $scope.model.filterAdvanced = true; - } else if (angular.isObject($scope.model.filter)) { + } else if (Utilities.isObject($scope.model.filter)) { $scope.model.filterAdvanced = true; } else { if ($scope.model.filter.startsWith('!')) { @@ -3777,8 +4435,15 @@ //used advanced filtering if ($scope.model.filter.startsWith('{')) { $scope.model.filterAdvanced = true; - //convert to object - $scope.model.filter = angular.fromJson($scope.model.filter); + if ($scope.model.filterByMetadata && !Utilities.isFunction($scope.model.filter)) { + var filter = Utilities.fromJson($scope.model.filter); + $scope.model.filter = function (node) { + return _.isMatch(node.metaData, filter); + }; + } else { + //convert to object + $scope.model.filter = Utilities.fromJson($scope.model.filter); + } } } } @@ -3852,13 +4517,13 @@ if (args.node.metaData.isContainer) { openMiniListView(args.node); } - if (angular.isArray(args.children)) { + if (Utilities.isArray(args.children)) { //iterate children - _.each(args.children, function (child) { + args.children.forEach(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(vm.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; + var exists = vm.searchInfo.selectedSearchResults.find(function (selected) { + return child.id === selected.id; }); if (exists) { child.selected = true; @@ -3967,7 +4632,7 @@ if ($scope.model.selection.length > 0) { for (var i = 0; $scope.model.selection.length > i; i++) { var selectedItem = $scope.model.selection[i]; - if (selectedItem.id === item.id) { + if (selectedItem.id === parseInt(item.id)) { found = true; foundIndex = i; } @@ -3986,12 +4651,12 @@ //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); + return !Utilities.isObject(n.metaData.listViewNode); }); if ($scope.model.filterAdvanced) { //filter either based on a method or an object - var filtered = angular.isFunction($scope.model.filter) ? _.filter(nodes, $scope.model.filter) : _.where(nodes, $scope.model.filter); - angular.forEach(filtered, function (value, key) { + var filtered = Utilities.isFunction($scope.model.filter) ? _.filter(nodes, $scope.model.filter) : _.where(nodes, $scope.model.filter); + filtered.forEach(function (value) { value.filtered = true; if ($scope.model.filterCssClass) { if (!value.cssClasses) { @@ -4003,7 +4668,7 @@ }); } else { var a = $scope.model.filter.toLowerCase().replace(/\s/g, '').split(','); - angular.forEach(nodes, function (value, key) { + nodes.forEach(function (value) { var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; if (!$scope.model.filterExclude && !found || $scope.model.filterExclude && found) { value.filtered = true; @@ -4019,6 +4684,7 @@ } } function openMiniListView(node) { + previouslyFocusedElement = document.activeElement; vm.miniListView = node; } function multiSubmit(result) { @@ -4056,10 +4722,10 @@ //we need to ensure that any currently displayed nodes that get selected // from the search get updated to have a check box! var checkChildren = function checkChildren(children) { - _.each(children, function (child) { + children.forEach(function (child) { //check if the id is in the selection, if so ensure it's flagged as selected - var exists = _.find(vm.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; + var exists = vm.searchInfo.selectedSearchResults.find(function (selected) { + return child.id === selected.id; }); //if the curr node exists in selected search results, ensure it's checked if (exists) { @@ -4080,12 +4746,14 @@ child.cssClasses = _.reject(child.cssClasses, function (c) { return c === 'tree-node-slide-up-hide-active'; }); - var listViewResults = _.filter(vm.searchInfo.selectedSearchResults, function (i) { - return i.parentId == child.id; + var listViewResults = vm.searchInfo.selectedSearchResults.filter(function (i) { + return i.parentId === child.id; }); - _.each(listViewResults, function (item) { - var childExists = _.find(child.children, function (c) { - return c.id == item.id; + listViewResults.forEach(function (item) { + if (!child.children) + return; + var childExists = child.children.find(function (c) { + return c.id === item.id; }); if (!childExists) { var _parent = child; @@ -4120,14 +4788,14 @@ //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) { + results = results.filter(function (item) { return !item.filtered; }); vm.searchInfo.results = results; //sync with the curr selected results - _.each(vm.searchInfo.results, function (result) { - var exists = _.find($scope.model.selection, function (selectedId) { - return result.id == selectedId; + vm.searchInfo.results.forEach(function (result) { + var exists = $scope.model.selection.find(function (item) { + return result.id === item.id; }); if (exists) { result.selected = true; @@ -4142,6 +4810,20 @@ } function closeMiniListView() { vm.miniListView = undefined; + if (previouslyFocusedElement) { + $timeout(function () { + previouslyFocusedElement.focus(); + previouslyFocusedElement = null; + }); + } + } + function listViewItemsLoaded(items) { + var selectedIds = $scope.model.selection.map(function (x) { + return x.id; + }); + items.forEach(function (item) { + return item.selected = selectedIds.includes(item.id); + }); } function submit(model) { if ($scope.model.submit) { @@ -4189,8 +4871,8 @@ }); } function preSelect(selection) { - angular.forEach(selection, function (selected) { - angular.forEach(vm.userGroups, function (userGroup) { + selection.forEach(function (selected) { + vm.userGroups.forEach(function (userGroup) { if (selected.id === userGroup.id) { userGroup.selected = true; } @@ -4202,7 +4884,7 @@ userGroup.selected = true; $scope.model.selection.push(userGroup); } else { - angular.forEach($scope.model.selection, function (selectedUserGroup, index) { + $scope.model.selection.forEach(function (selectedUserGroup, index) { if (selectedUserGroup.id === userGroup.id) { userGroup.selected = false; $scope.model.selection.splice(index, 1); @@ -4227,7 +4909,7 @@ 'use strict'; (function () { 'use strict'; - function UserPickerController($scope, usersResource, localizationService) { + function UserPickerController($scope, entityResource, localizationService, eventsService) { var vm = this; vm.users = []; vm.loading = false; @@ -4237,12 +4919,13 @@ vm.changePageNumber = changePageNumber; vm.submit = submit; vm.close = close; - ////////// + vm.multiPicker = $scope.model.multiPicker === false ? false : true; function onInit() { vm.loading = true; // set default title if (!$scope.model.title) { - localizationService.localize('defaultdialogs_selectUsers').then(function (value) { + var labelKey = vm.multiPicker ? 'defaultdialogs_selectUsers' : 'defaultdialogs_selectUser'; + localizationService.localize(labelKey).then(function (value) { $scope.model.title = value; }); } @@ -4254,8 +4937,8 @@ getUsers(); } function preSelect(selection, users) { - angular.forEach(selection, function (selected) { - angular.forEach(users, function (user) { + Utilities.forEach(selection, function (selected) { + Utilities.forEach(users, function (user) { if (selected.id === user.id) { user.selected = true; } @@ -4267,14 +4950,33 @@ 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); + if (user.selected) { + Utilities.forEach($scope.model.selection, function (selectedUser, index) { + if (selectedUser.id === user.id) { + user.selected = false; + $scope.model.selection.splice(index, 1); + } + }); + } else { + if (!vm.multiPicker) { + deselectAllUsers($scope.model.selection); } - }); + eventsService.emit('dialogs.userPicker.select', user); + user.selected = true; + $scope.model.selection.push(user); + } + } + if (!vm.multiPicker) { + submit($scope.model); } } + function deselectAllUsers(users) { + for (var i = 0; i < users.length; i++) { + var user = users[i]; + user.selected = false; + } + users.length = 0; + } var search = _.debounce(function () { $scope.$apply(function () { getUsers(); @@ -4286,12 +4988,8 @@ 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; + entityResource.getAll('User').then(function (data) { + vm.users = data; preSelect($scope.model.selection, vm.users); vm.loading = false; }); @@ -4401,6 +5099,7 @@ }); 'use strict'; function ItemPickerOverlay($scope, localizationService) { + $scope.filter = { searchTerm: '' }; function onInit() { $scope.model.hideSubmitButton = true; if (!$scope.model.title) { @@ -4416,19 +5115,32 @@ $scope.model.selectedItem = item; $scope.submitForm($scope.model); }; + $scope.tooltip = { + show: false, + event: null + }; + $scope.showTooltip = function (item, $event) { + if (!item.tooltip) { + return; + } + $scope.tooltip = { + show: true, + event: $event, + text: item.tooltip + }; + }; + $scope.hideTooltip = function () { + $scope.tooltip = { + show: false, + event: null, + text: null + }; + }; onInit(); } angular.module('umbraco').controller('Umbraco.Overlays.ItemPickerOverlay', ItemPickerOverlay); 'use strict'; - 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; - }; - }); - 'use strict'; - angular.module('umbraco').controller('Umbraco.Overlays.UserController', function ($scope, $location, $timeout, dashboardResource, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper, localizationService) { + angular.module('umbraco').controller('Umbraco.Overlays.UserController', function ($scope, $location, $timeout, dashboardResource, userService, historyService, eventsService, externalLoginInfo, externalLoginInfoService, authResource, currentUserResource, formHelper, localizationService) { $scope.history = historyService.getCurrent(); //$scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; $scope.showPasswordFields = false; @@ -4442,7 +5154,10 @@ }); } */ - $scope.externalLoginProviders = externalLoginInfo.providers; + // Set flag if any have deny local login, in which case we must disable all password functionality + $scope.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); + // Only include login providers that have editable options + $scope.externalLoginProviders = externalLoginInfoService.getLoginProvidersWithOptions(); $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; var evts = []; evts.push(eventsService.on('historyService.add', function (e, args) { @@ -4494,9 +5209,9 @@ //updateTimeout(); authResource.getCurrentUserLinkedLogins().then(function (logins) { //reset all to be un-linked - for (var provider in $scope.externalLoginProviders) { - $scope.externalLoginProviders[provider].linkedProviderKey = undefined; - } + $scope.externalLoginProviders.forEach(function (provider) { + return provider.linkedProviderKey = undefined; + }); //set the linked logins for (var login in logins) { var found = _.find($scope.externalLoginProviders, function (i) { @@ -4510,6 +5225,9 @@ } }); } + $scope.linkProvider = function (e) { + e.target.submit(); + }; $scope.unlink = function (e, loginProvider, providerKey) { var result = confirm('Are you sure you want to unlink this account?'); if (!result) { @@ -4559,6 +5277,10 @@ $scope.togglePasswordFields(); }, 2000); }, function (err) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); formHelper.handleError(err); $scope.changePasswordButtonState = 'error'; }); @@ -4604,6 +5326,18 @@ onInit(); }); 'use strict'; + (function () { + 'use strict'; + function ConfirmController($scope, userService) { + var vm = this; + vm.userEmailAddress = ''; + userService.getCurrentUser().then(function (user) { + vm.userEmailAddress = user.email; + }); + } + angular.module('umbraco').controller('Umbraco.Tours.UmbEmailMarketing.ConfirmController', ConfirmController); + }()); + 'use strict'; (function () { 'use strict'; function EmailsController($scope, userService) { @@ -4615,10 +5349,7 @@ // It's a fire & forget - not sure we need to check the response userService.addUserToEmailMarketing(user); }); - // Mark Tour as complete - // This is also can help us indicate that the user accepted - // Where disabled is set if user closes modal or chooses NO - $scope.model.completeTour(); + $scope.model.nextStep(); }; } angular.module('umbraco').controller('Umbraco.Tours.UmbEmailMarketing.EmailsController', EmailsController); @@ -4628,7 +5359,7 @@ 'use strict'; function NodeNameController($scope) { var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { @@ -4646,7 +5377,7 @@ 'use strict'; function DocTypeNameController($scope) { var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { @@ -4664,7 +5395,7 @@ 'use strict'; function PropertyNameController($scope) { var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { @@ -4682,7 +5413,7 @@ 'use strict'; function TabNameController($scope) { var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { @@ -4700,7 +5431,7 @@ 'use strict'; function FolderNameController($scope) { var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { @@ -4718,7 +5449,6 @@ 'use strict'; function UploadImagesController($scope, editorState, mediaResource) { var vm = this; - var element = angular.element($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { @@ -4743,8 +5473,7 @@ (function () { 'use strict'; function TemplatesTreeController($scope) { - var vm = this; - var eventElement = angular.element($scope.model.currentStep.eventElement); + var eventElement = $($scope.model.currentStep.eventElement); function onInit() { // check if tree is already open - if it is - go to next step if (eventElement.hasClass('icon-navigation-down')) { @@ -4763,30 +5492,54 @@ //if we make the viewModel the variant itself, we end up with a circular reference in the models which isn't ideal // (i.e. variant.apps[contentApp].viewModel = variant) //so instead since we already have access to the content, we can just get the variant directly by the index. + var unbindLanguageWatcher = function unbindLanguageWatcher() { + }; + var unbindSegmentWatcher = function unbindSegmentWatcher() { + }; + var timeout = null; var vm = this; vm.loading = true; function onInit() { - //get the variant by index (see notes above) - vm.content = $scope.content.variants[$scope.model.viewModel]; serverValidationManager.notify(); vm.loading = false; + timeout = null; + // ensure timeout is set to null, so we know that its not running anymore. //if this variant has a culture/language assigned, then we need to watch it since it will change //if the language drop down changes and we need to re-init - if (vm.content.language) { - $scope.$watch(function () { - return vm.content.language.culture; - }, function (newVal, oldVal) { - if (newVal !== oldVal) { - vm.loading = true; - // TODO: Can we minimize the flicker? - $timeout(function () { - onInit(); - }, 100); + if ($scope.variantContent) { + if ($scope.variantContent.language) { + unbindLanguageWatcher = $scope.$watch(function () { + return $scope.variantContent.language.culture; + }, function (newVal, oldVal) { + if (newVal !== oldVal) { + requestUpdate(); + } + }); + } + unbindSegmentWatcher = $scope.$watch(function () { + return $scope.variantContent.segment; + }, function (newVal, oldVal) { + if (newVal !== oldVal) { + requestUpdate(); } }); } } + function requestUpdate() { + if (timeout === null) { + vm.loading = true; + // TODO: Can we minimize the flicker? + timeout = $timeout(function () { + onInit(); + }, 100); + } + } onInit(); + $scope.$on('$destroy', function () { + unbindLanguageWatcher(); + unbindSegmentWatcher(); + $timeout.cancel(timeout); + }); } angular.module('umbraco').controller('Umbraco.Editors.Content.Apps.ContentController', ContentAppContentController); }()); @@ -5050,7 +5803,7 @@ // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); + nodeSelectHandler({ node: node }); }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; @@ -5064,7 +5817,7 @@ * @ngdoc controller * @name Umbraco.Editors.Content.CreateController * @function - * + * * @description * The controller for the content creation dialog */ @@ -5073,15 +5826,14 @@ function initialize() { $scope.loading = true; $scope.allowedTypes = null; - $scope.countTypes = contentTypeResource.getCount; var getAllowedTypes = contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); }); var getCurrentUser = authResource.getCurrentUser().then(function (currentUser) { - if (currentUser.allowedSections.indexOf('settings') > -1) { - $scope.hasSettingsAccess = true; + $scope.hasSettingsAccess = currentUser.allowedSections.indexOf('settings') > -1; + if ($scope.hasSettingsAccess) { if ($scope.currentNode.id > -1) { - contentResource.getById($scope.currentNode.id).then(function (data) { + return contentResource.getById($scope.currentNode.id).then(function (data) { $scope.contentTypeId = data.contentTypeId; }); } @@ -5091,6 +5843,12 @@ getAllowedTypes, getCurrentUser ]).then(function () { + if ($scope.hasSettingsAccess === true && $scope.allowedTypes.length === 0) { + return contentTypeResource.getCount().then(function (count) { + $scope.countTypes = count; + }); + } + }).then(function () { $scope.loading = false; }); $scope.selectContentType = true; @@ -5101,8 +5859,9 @@ navigationService.hideMenu(); } function createBlank(docType) { - $location.path('/content/content/edit/' + $scope.currentNode.id).search('doctype', docType.alias).search('create', 'true') /* when we create a new node we want to make sure it uses the same - language as what is selected in the tree */.search('cculture', mainCulture) /* when we create a new node we must make sure that any previously + $location.path('/content/content/edit/' + $scope.currentNode.id).search('doctype', docType.alias).search('create', 'true') /* when we create a new node we want to make sure it uses the same + language as what is selected in the tree */.search('cculture', mainCulture) /* when we create a new node we must make sure that any previously + opened segments is reset */.search('csegment', null) /* when we create a new node we must make sure that any previously used blueprint is reset */.search('blueprintId', null); close(); } @@ -5148,7 +5907,7 @@ $scope.createBlank = createBlank; $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; $scope.createFromBlueprint = createFromBlueprint; - // the current node changes behind the scenes when the context menu is clicked without closing + // the current node changes behind the scenes when the context menu is clicked without closing // the default menu first, so we must watch the current node and re-initialize accordingly var unbindModelWatcher = $scope.$watch('currentNode', initialize); $scope.$on('$destroy', function () { @@ -5179,6 +5938,10 @@ formHelper.resetForm({ scope: $scope }); navigationService.hideMenu(); }, function (err) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); contentEditingHelper.handleSaveError({ err: err }); }); } @@ -5246,6 +6009,10 @@ if (err.status && err.status >= 500) { // TODO: All YSOD handling should be done with an interceptor overlayService.ysod(err); + navigationService.hideDialog(); + } + if (err.data && err.data.notifications && err.data.notifications.length > 0) { + navigationService.hideDialog(); } }); }; @@ -5286,6 +6053,7 @@ $scope.isNew = infiniteMode ? $scope.model.create : $routeParams.create; //load the default culture selected in the main tree if any $scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + $scope.segment = $routeParams.csegment ? $routeParams.csegment : null; //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. //This is so we can listen to changes on the cculture parameter since that will not cause a route change //and then we can pass in the updated culture to the editor. @@ -5293,6 +6061,7 @@ //will not cause a route change and so we can update the isNew and contentId flags accordingly. $scope.$on('$routeUpdate', function (event, next) { $scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture; + $scope.segment = next.params.csegment ? next.params.csegment : null; $scope.isNew = next.params.create === 'true'; $scope.contentId = infiniteMode ? $scope.model.id : $routeParams.id; }); @@ -5481,7 +6250,7 @@ vm.saveError = false; vm.saveSuccces = false; var selectedString = []; - angular.forEach(notifyOptions, function (option) { + notifyOptions.forEach(function (option) { if (option.checked === true && option.notifyCode) { selectedString.push(option.notifyCode); } @@ -5959,7 +6728,7 @@ //reset this vm.selectedUserGroups = []; vm.availableUserGroups = userGroups; - angular.forEach(vm.availableUserGroups, function (group) { + vm.availableUserGroups.forEach(function (group) { if (group.permissions) { //if there's explicit permissions assigned than it's selected assignGroupPermissions(group); @@ -5989,8 +6758,8 @@ // 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) { + Object.values(group.permissions).forEach(function (permissionGroup) { + permissionGroup.forEach(function (permission) { if (permission.checked) { //the `allowedPermissions` is what will get sent up to the server for saving group.allowedPermissions.push(permission); @@ -6031,9 +6800,9 @@ setViewSate('manageGroups'); } function formatSaveModel(permissionsSave, groupCollection) { - angular.forEach(groupCollection, function (g) { + groupCollection.forEach(function (g) { permissionsSave[g.id] = []; - angular.forEach(g.allowedPermissions, function (p) { + g.allowedPermissions.forEach(function (p) { permissionsSave[g.id].push(p.permissionCode); }); }); @@ -6061,6 +6830,7 @@ currentForm.$dirty = false; } }); + $scope.dialog.confirmDiscardChanges = false; vm.saveState = 'success'; vm.saveSuccces = true; }, function (error) { @@ -6109,7 +6879,7 @@ 'use strict'; (function () { 'use strict'; - function ContentSortController($scope, $filter, $routeParams, contentResource, navigationService) { + function ContentSortController($scope, $filter, $routeParams, contentResource, navigationService, eventsService) { var vm = this; var id = $scope.currentNode.id; vm.loading = false; @@ -6117,6 +6887,8 @@ vm.saveButtonState = 'init'; vm.sortOrder = {}; vm.sortableOptions = { + axis: 'y', + containment: 'parent', distance: 10, tolerance: 'pointer', opacity: 0.7, @@ -6155,6 +6927,7 @@ }).then(function () { return navigationService.reloadNode($scope.currentNode); }); + eventsService.emit('sortCompleted', { id: id }); vm.saveButtonState = 'success'; }, function (error) { vm.error = error; @@ -6191,153 +6964,134 @@ function PublishController($scope, localizationService, contentEditingHelper) { var vm = this; vm.loading = true; - vm.hasPristineVariants = false; vm.isNew = true; vm.changeSelection = changeSelection; - vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; - /** Returns true if publishing is possible based on if there are un-published mandatory languages */ + /** + * Returns true if publish meets the requirements of mandatory languages + * */ function canPublish() { - var possible = false; - for (var i = 0; i < vm.variants.length; i++) { - var variant = vm.variants[i]; - var state = canVariantPublish(variant); - if (state === true) { - possible = true; - } - if (state === false) { + var hasSomethingToPublish = false; + vm.variants.forEach(function (variant) { + // if varaint is mandatory and not already published: + if (variant.publish === false && notPublishedMandatoryFilter(variant)) { return false; } - } - return possible; - } - /** Returns true if publishing is possible based on if the variant is a un-published mandatory language */ - function canVariantPublish(variant) { - //if this variant will show up in the publish-able list - var publishable = dirtyVariantFilter(variant); - var published = !(variant.state === 'NotCreated' || variant.state === 'Draft'); - // is this variant mandatory: - if (variant.language.isMandatory && !published && !variant.publish) { - //if a mandatory variant isn't published or set to be published - //then we cannot continue - return false; - } - // is this variant selected for publish: - if (variant.publish === true) { - return publishable; - } - return null; + if (variant.publish === true) { + hasSomethingToPublish = true; + } + }); + return hasSomethingToPublish; } function changeSelection(variant) { + // update submit button state: $scope.model.disableSubmitButton = !canPublish(); - //need to set the Save state to true if publish is true + //need to set the Save state to same as publish. variant.save = variant.publish; - variant.willPublish = canVariantPublish(variant); - } - function dirtyVariantFilter(variant) { - //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's - // * the active one - // * it's editor is in a $dirty state - // * it has pending saves - // * it is unpublished - // * it is in NotCreated state - return variant.active || variant.isDirty || variant.state === 'Draft' || variant.state === 'PublishedPendingChanges' || variant.state === 'NotCreated'; } - function hasAnyData(variant) { - if (variant.name == null || variant.name.length === 0) { - return false; + function hasAnyDataFilter(variant) { + // if we have a name, then we have data. + if (variant.name != null && variant.name.length > 0) { + return true; } - var result = variant.isDirty != null; - if (result) + if (variant.isDirty === true) { return true; - for (var t = 0; t < variant.tabs.length; t++) { - for (var p = 0; p < variant.tabs[t].properties.length; p++) { - var property = variant.tabs[t].properties[p]; - if (property.culture == null) - continue; - result = result || property.value != null && property.value.length > 0; - if (result) - return true; - } } - return result; + variant.tabs.forEach(function (tab) { + tab.properties.forEach(function (property) { + if (property.value != null && property.value.length > 0) { + return true; + } + }); + }); + return false; + } + /** + * determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + * * it's editor is in a $dirty state + * * it has pending saves + * * it is unpublished + * @param {*} variant + */ + function dirtyVariantFilter(variant) { + return variant.isDirty || variant.state === 'Draft' || variant.state === 'PublishedPendingChanges'; + } + /** + * determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + * * variant is active + * * it's editor is in a $dirty state + * * it has pending saves + * * it is unpublished + * @param {*} variant + */ + function publishableVariantFilter(variant) { + return variant.active || variant.isDirty || variant.state === 'Draft' || variant.state === 'PublishedPendingChanges'; + } + function notPublishedMandatoryFilter(variant) { + return variant.state !== 'Published' && variant.state !== 'PublishedPendingChanges' && variant.isMandatory === true; + } + /** + * determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + * * has a mandatory language + * * without having a segment, segments cant be mandatory at current state of code. + * @param {*} variant + */ + function isMandatoryFilter(variant) { + return variant.language && variant.language.isMandatory === true && variant.segment == null; } - function pristineVariantFilter(variant) { - return !dirtyVariantFilter(variant); + /** + * determine a variant is needed, but not already a choice. + * * publishable — aka. displayed as a publish option. + * * published — its already published and everything is then fine. + * * mandatory — this is needed, and thats why we highlight it. + * @param {*} variant + */ + function notPublishableButMandatoryFilter(variant) { + return !publishableVariantFilter(variant) && variant.state !== 'Published' && variant.isMandatory === true; } function onInit() { vm.variants = $scope.model.variants; - if (!$scope.model.title) { - localizationService.localize('content_readyToPublish').then(function (value) { - $scope.model.title = value; - }); - } - vm.hasPristineVariants = false; - _.each(vm.variants, function (variant) { - if (variant.state === 'NotCreated') { - vm.isNew = true; - } + // If we have a variant that's not in the state of NotCreated, + // then we know we have data and it's not a new content node. + vm.isNew = vm.variants.some(function (variant) { + return variant.state === 'NotCreated'; }); - _.each(vm.variants, function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = '_content_variant_' + variant.compositeId; + vm.variants.forEach(function (variant) { // reset to not be published - variant.publish = false; - variant.save = false; - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); - } - // If the variant havent been created jet. - if (variant.state === 'NotCreated') { - // If the variant is mandatory, then set the variant to be published. - if (variant.language.isMandatory === true) { - variant.publish = true; - variant.save = true; - } + variant.publish = variant.save = false; + variant.isMandatory = isMandatoryFilter(variant); + // if this is a new node and we have data on this variant. + if (vm.isNew === true && hasAnyDataFilter(variant)) { + variant.save = true; } - variant.canPublish = dirtyVariantFilter(variant); - // if we have data on this variant. - if (variant.canPublish && hasAnyData(variant)) { - // and if some varaints havent been saved before, or they dont have a publishing date set, then we set it for publishing. - if (vm.isNew || variant.publishDate == null) { - variant.publish = true; - variant.save = true; - } + }); + vm.availableVariants = vm.variants.filter(publishableVariantFilter); + vm.missingMandatoryVariants = vm.variants.filter(notPublishableButMandatoryFilter); + // if any active varaiant that is available for publish, we set it to be published: + vm.availableVariants.forEach(function (v) { + if (v.active) { + v.save = v.publish = true; } - variant.willPublish = canVariantPublish(variant); }); - if (vm.variants.length !== 0) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; - }); - var active = _.find(vm.variants, function (v) { - return v.active; + if (vm.availableVariants.length !== 0) { + vm.availableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.availableVariants); + } + $scope.model.disableSubmitButton = !canPublish(); + var localizeKey = vm.missingMandatoryVariants.length > 0 ? 'content_notReadyToPublish' : !$scope.model.title ? 'content_readyToPublish' : ''; + if (localizeKey) { + localizationService.localize(localizeKey).then(function (value) { + $scope.model.title = value; + vm.loading = false; }); - if (active) { - //ensure that the current one is selected - active.publish = true; - active.save = true; - } - $scope.model.disableSubmitButton = !canPublish(); } else { - //disable Publish button if we have nothing to publish, should not happen - $scope.model.disableSubmitButton = true; - } - var labelKey = vm.isNew ? 'content_languagesToPublishForFirstTime' : 'content_languagesToPublish'; - localizationService.localize(labelKey).then(function (value) { - vm.headline = value; vm.loading = false; - }); + } } onInit(); //when this dialog is closed, reset all 'publish' flags $scope.$on('$destroy', function () { - for (var i = 0; i < vm.variants.length; i++) { - vm.variants[i].publish = false; - vm.variants[i].save = false; - } + vm.variants.forEach(function (variant) { + variant.publish = variant.save = false; + }); }); } angular.module('umbraco').controller('Umbraco.Overlays.PublishController', PublishController); @@ -6345,53 +7099,64 @@ 'use strict'; (function () { 'use strict'; - function PublishDescendantsController($scope, localizationService) { + function PublishDescendantsController($scope, localizationService, contentEditingHelper) { var vm = this; + vm.includeUnpublished = $scope.model.includeUnpublished || false; vm.changeSelection = changeSelection; + vm.toggleIncludeUnpublished = toggleIncludeUnpublished; function onInit() { - vm.includeUnpublished = false; vm.variants = $scope.model.variants; + vm.displayVariants = vm.variants.slice(0); + // shallow copy, we don't want to share the array-object (because we will be performing a sort method) but each entry should be shared (because we need validation and notifications). vm.labels = {}; + // get localized texts for use in directives if (!$scope.model.title) { localizationService.localize('buttons_publishDescendants').then(function (value) { $scope.model.title = value; }); } - _.each(vm.variants, function (variant) { - variant.compositeId = (variant.language ? variant.language.culture : 'inv') + '_' + (variant.segment ? variant.segment : ''); - variant.htmlId = '_content_variant_' + variant.compositeId; + if (!vm.labels.includeUnpublished) { + localizationService.localize('content_includeUnpublished').then(function (value) { + vm.labels.includeUnpublished = value; + }); + } + if (!vm.labels.includeUnpublished) { + localizationService.localize('content_includeUnpublished').then(function (value) { + vm.labels.includeUnpublished = value; + }); + } + vm.variants.forEach(function (variant) { + variant.isMandatory = isMandatoryFilter(variant); }); if (vm.variants.length > 1) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; - }); - var active = _.find(vm.variants, function (v) { + vm.displayVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.displayVariants); + var active = vm.variants.find(function (v) { return v.active; }); if (active) { //ensure that the current one is selected - active.publish = true; - active.save = true; + active.publish = active.save = true; } $scope.model.disableSubmitButton = !canPublish(); } else { // localize help text for invariant content vm.labels.help = { 'key': 'content_publishDescendantsHelp', - 'tokens': [] + 'tokens': [vm.variants[0].name] }; - // add the node name as a token so it will show up in the translated text - vm.labels.help.tokens.push(vm.variants[0].name); } } + function toggleIncludeUnpublished() { + vm.includeUnpublished = !vm.includeUnpublished; + // make sure this value is pushed back to the scope + $scope.model.includeUnpublished = vm.includeUnpublished; + } /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canPublish() { var selected = []; - for (var i = 0; i < vm.variants.length; i++) { - var variant = vm.variants[i]; + vm.variants.forEach(function (variant) { var published = !(variant.state === 'NotCreated' || variant.state === 'Draft'); - if (variant.language.isMandatory && !published && !variant.publish) { + if (variant.segment == null && variant.language && variant.language.isMandatory && !published && !variant.publish) { //if a mandatory variant isn't published //and not flagged for saving //then we cannot continue @@ -6401,7 +7166,7 @@ if (variant.publish) { selected.push(variant.publish); } - } + }); return selected.length > 0; } function changeSelection(variant) { @@ -6409,12 +7174,17 @@ //need to set the Save state to true if publish is true variant.save = variant.publish; } + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return variant.language && variant.language.isMandatory === true && variant.segment == null; + } //when this dialog is closed, reset all 'publish' flags $scope.$on('$destroy', function () { - for (var i = 0; i < vm.variants.length; i++) { - vm.variants[i].publish = false; - vm.variants[i].save = false; - } + vm.variants.forEach(function (variant) { + variant.publish = variant.save = false; + }); }); onInit(); } @@ -6429,23 +7199,23 @@ vm.hasPristineVariants = false; vm.isNew = true; vm.changeSelection = changeSelection; - vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; function changeSelection(variant) { var firstSelected = _.find(vm.variants, function (v) { return v.save; }); $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected } - function dirtyVariantFilter(variant) { + function saveableVariantFilter(variant) { //determine a variant is 'dirty' (meaning it will show up as save-able) if it's // * the active one // * it's editor is in a $dirty state - // * it is in NotCreated state return variant.active || variant.isDirty; } - function pristineVariantFilter(variant) { - return !dirtyVariantFilter(variant); + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return variant.language && variant.language.isMandatory === true && variant.segment == null; } function hasAnyData(variant) { if (variant.name == null || variant.name.length === 0) { @@ -6468,40 +7238,32 @@ } function onInit() { vm.variants = $scope.model.variants; + vm.availableVariants = vm.variants.filter(saveableVariantFilter); + vm.isNew = vm.variants.some(function (variant) { + return variant.state === 'NotCreated'; + }); if (!$scope.model.title) { localizationService.localize('content_readyToSave').then(function (value) { $scope.model.title = value; }); } - vm.hasPristineVariants = false; - _.each(vm.variants, function (variant) { - if (variant.state !== 'NotCreated') { - vm.isNew = false; - } - }); - _.each(vm.variants, function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = '_content_variant_' + variant.compositeId; - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); - } + vm.variants.forEach(function (variant) { + //reset state: + variant.save = variant.publish = false; + variant.isMandatory = isMandatoryFilter(variant); if (vm.isNew && hasAnyData(variant)) { variant.save = true; } }); if (vm.variants.length !== 0) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; - }); - var active = _.find(vm.variants, function (v) { + //ensure that the current one is selected + var active = vm.variants.find(function (v) { return v.active; }); if (active) { - //ensure that the current one is selected active.save = true; } + vm.availableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.availableVariants); } else { //disable save button if we have nothing to save $scope.model.disableSubmitButton = true; @@ -6511,9 +7273,9 @@ onInit(); //when this dialog is closed, reset all 'save' flags $scope.$on('$destroy', function () { - for (var i = 0; i < vm.variants.length; i++) { - vm.variants[i].save = false; - } + vm.variants.forEach(function (variant) { + variant.save = false; + }); }); } angular.module('umbraco').controller('Umbraco.Overlays.SaveContentController', SaveContentController); @@ -6530,7 +7292,6 @@ vm.clearPublishDate = clearPublishDate; vm.clearUnpublishDate = clearUnpublishDate; vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; vm.changeSelection = changeSelection; vm.firstSelectedDates = {}; vm.currentUser = null; @@ -6538,46 +7299,35 @@ var origDates = []; function onInit() { vm.variants = $scope.model.variants; - vm.hasPristineVariants = false; - for (var i = 0; i < vm.variants.length; i++) { - origDates.push({ - releaseDate: vm.variants[i].releaseDate, - expireDate: vm.variants[i].expireDate - }); - } + vm.displayVariants = vm.variants.slice(0); + // shallow copy, we dont want to share the array-object(because we will be performing a sort method) but each entry should be shared (because we need validation and notifications). if (!$scope.model.title) { localizationService.localize('general_scheduledPublishing').then(function (value) { $scope.model.title = value; }); } + vm.variants.forEach(function (variant) { + origDates.push({ + releaseDate: variant.releaseDate, + expireDate: variant.expireDate + }); + variant.isMandatory = isMandatoryFilter(variant); + }); // Check for variants: if a node is invariant it will still have the default language in variants // so we have to check for length > 1 if (vm.variants.length > 1) { - _.each(vm.variants, function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = '_content_variant_' + variant.compositeId; - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); + vm.displayVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.displayVariants); + vm.variants.forEach(function (v) { + if (v.active) { + v.save = true; } }); - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; - }); - var active = _.find(vm.variants, function (v) { - return v.active; - }); - if (active) { - //ensure that the current one is selected - active.save = true; - } $scope.model.disableSubmitButton = !canSchedule(); } // get current backoffice user and format dates userService.getCurrentUser().then(function (currentUser) { vm.currentUser = currentUser; - angular.forEach(vm.variants, function (variant) { + vm.variants.forEach(function (variant) { // prevent selecting publish/unpublish date before today var now = new Date(); var nowFormatted = moment(now).format('YYYY-MM-DD HH:mm'); @@ -6633,10 +7383,20 @@ * @param {any} type publish or unpublish */ function datePickerShow(variant, type) { + var activeDatePickerInstance; if (type === 'publish') { variant.releaseDatePickerOpen = true; + activeDatePickerInstance = variant.releaseDatePickerInstance; } else if (type === 'unpublish') { variant.expireDatePickerOpen = true; + activeDatePickerInstance = variant.expireDatePickerInstance; + } + // Prevent enter key in time fields from submitting the overlay before the associated input gets the updated time + if (activeDatePickerInstance && !activeDatePickerInstance.hourElement.hasAttribute('overlay-submit-on-enter')) { + activeDatePickerInstance.hourElement.setAttribute('overlay-submit-on-enter', 'false'); + } + if (activeDatePickerInstance && !activeDatePickerInstance.minuteElement.hasAttribute('overlay-submit-on-enter')) { + activeDatePickerInstance.minuteElement.setAttribute('overlay-submit-on-enter', 'false'); } checkForBackdropClick(); $scope.model.disableSubmitButton = !canSchedule(); @@ -6661,7 +7421,7 @@ * Prevent the overlay from closing if any date pickers are open */ function checkForBackdropClick() { - var open = _.find(vm.variants, function (variant) { + var open = vm.variants.find(function (variant) { return variant.releaseDatePickerOpen || variant.expireDatePickerOpen; }); if (open) { @@ -6764,8 +7524,11 @@ // * it is in NotCreated state return variant.active || variant.isDirty || variant.state === 'Draft' || variant.state === 'PublishedPendingChanges' || variant.state === 'NotCreated'; } - function pristineVariantFilter(variant) { - return !dirtyVariantFilter(variant); + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return variant.language && variant.language.isMandatory === true && variant.segment == null; } /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canSchedule() { @@ -6782,7 +7545,7 @@ if (schedCleared) { return true; } - var isMandatory = variant.language && variant.language.isMandatory; + var isMandatory = variant.segment == null && variant.language && variant.language.isMandatory; //if this variant will show up in the publish-able list var publishable = dirtyVariantFilter(variant); var published = !(variant.state === 'NotCreated' || variant.state === 'Draft'); @@ -6803,17 +7566,17 @@ onInit(); //when this dialog is closed, clean up $scope.$on('$destroy', function () { - for (var i = 0; i < vm.variants.length; i++) { - vm.variants[i].save = false; + vm.variants.forEach(function (variant) { + variant.save = false; // remove properties only needed for this dialog - delete vm.variants[i].releaseDateFormatted; - delete vm.variants[i].expireDateFormatted; - delete vm.variants[i].datePickerConfig; - delete vm.variants[i].releaseDatePickerInstance; - delete vm.variants[i].expireDatePickerInstance; - delete vm.variants[i].releaseDatePickerOpen; - delete vm.variants[i].expireDatePickerOpen; - } + delete variant.releaseDateFormatted; + delete variant.expireDateFormatted; + delete variant.datePickerConfig; + delete variant.releaseDatePickerInstance; + delete variant.expireDatePickerInstance; + delete variant.releaseDatePickerOpen; + delete variant.expireDatePickerOpen; + }); }); } angular.module('umbraco').controller('Umbraco.Overlays.ScheduleContentController', ScheduleContentController); @@ -6824,8 +7587,6 @@ function SendToPublishController($scope, localizationService, contentEditingHelper) { var vm = this; vm.loading = true; - vm.modifiedVariantFilter = modifiedVariantFilter; - vm.unmodifiedVariantFilter = unmodifiedVariantFilter; vm.changeSelection = changeSelection; function onInit() { vm.variants = $scope.model.variants; @@ -6835,22 +7596,17 @@ $scope.model.title = value; }); } - if (vm.variants.length !== 0) { - _.each(vm.variants, function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = '_content_variant_' + variant.compositeId; - }); - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; - }); - var active = _.find(vm.variants, function (v) { - return v.active; + vm.variants.forEach(function (variant) { + variant.isMandatory = isMandatoryFilter(variant); + }); + vm.availableVariants = vm.variants.filter(publishableVariantFilter); + if (vm.availableVariants.length !== 0) { + vm.availableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.availableVariants); + vm.availableVariants.forEach(function (v) { + if (v.active) { + v.save = true; + } }); - if (active) { - //ensure that the current one is selected - active.save = true; - } } else { //disable save button if we have nothing to save $scope.model.disableSubmitButton = true; @@ -6858,30 +7614,30 @@ vm.loading = false; } function changeSelection() { - var firstSelected = _.find(vm.variants, function (v) { + var firstSelected = vm.variants.find(function (v) { return v.save; }); $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected } - function modifiedVariantFilter(variant) { - //determine a variant is 'modified' (meaning it will show up as able to send for approval) - // * it's editor is in a $dirty state - // * it is in Draft state - // * it is published with pending changes - return variant.active || variant.isDirty || variant.state === 'Draft' || variant.state === 'PublishedPendingChanges'; + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return variant.language && variant.language.isMandatory === true && variant.segment == null; } - function unmodifiedVariantFilter(variant) { - //determine a variant is 'unmodified' (meaning it will NOT show up as able to send for approval) + function publishableVariantFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * variant is active // * it's editor is in a $dirty state - // * it has been published - // * it is not created for that specific language - return variant.state === 'Published' && !variant.isDirty && !variant.active || variant.state === 'NotCreated' && !variant.isDirty && !variant.active; + // * it has pending saves + // * it is unpublished + return variant.active || variant.isDirty || variant.state === 'Draft' || variant.state === 'PublishedPendingChanges'; } //when this dialog is closed, reset all 'save' flags $scope.$on('$destroy', function () { - for (var i = 0; i < vm.variants.length; i++) { - vm.variants[i].save = false; - } + vm.variants.forEach(function (variant) { + variant.save = false; + }); }); onInit(); } @@ -6894,30 +7650,25 @@ var vm = this; var autoSelectedVariants = []; vm.changeSelection = changeSelection; - vm.publishedVariantFilter = publishedVariantFilter; - vm.unpublishedVariantFilter = unpublishedVariantFilter; function onInit() { vm.variants = $scope.model.variants; + vm.unpublishableVariants = vm.variants.filter(publishedVariantFilter); // set dialog title if (!$scope.model.title) { localizationService.localize('content_unpublish').then(function (value) { $scope.model.title = value; }); } - _.each(vm.variants, function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = '_content_variant_' + variant.compositeId; + vm.variants.forEach(function (variant) { + variant.isMandatory = isMandatoryFilter(variant); }); // node has variants if (vm.variants.length !== 1) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; - }); - var active = _.find(vm.variants, function (v) { + vm.unpublishableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.unpublishableVariants); + var active = vm.variants.find(function (v) { return v.active; }); - if (active) { + if (active && publishedVariantFilter(active)) { //ensure that the current one is selected active.save = true; } @@ -6926,20 +7677,14 @@ } } function changeSelection(selectedVariant) { - // disable submit button if nothing is selected - var firstSelected = _.find(vm.variants, function (v) { - return v.save; - }); - $scope.model.disableSubmitButton = !firstSelected; - //disable submit button if there is none selected - // if a mandatory variant is selected we want to select all other variants + // if a mandatory variant is selected we want to select all other variants, we cant have anything published if a mandatory variants gets unpublished. // and disable selection for the others - if (selectedVariant.save && selectedVariant.language.isMandatory) { - angular.forEach(vm.variants, function (variant) { - if (!variant.save && publishedVariantFilter(variant)) { + if (selectedVariant.save && selectedVariant.segment == null && selectedVariant.language && selectedVariant.language.isMandatory) { + vm.variants.forEach(function (variant) { + if (!variant.save) { // keep track of the variants we automaically select // so we can remove the selection again - autoSelectedVariants.push(variant.language.culture); + autoSelectedVariants.push(variant); variant.save = true; } variant.disabled = true; @@ -6950,17 +7695,30 @@ // if a mandatory variant is deselected we want to deselet all the variants // that was automatically selected so it goes back to the state before the mandatory language was selected. // We also want to enable all checkboxes again - if (!selectedVariant.save && selectedVariant.language.isMandatory) { - angular.forEach(vm.variants, function (variant) { + if (!selectedVariant.save && selectedVariant.segment == null && selectedVariant.language && selectedVariant.language.isMandatory) { + vm.variants.forEach(function (variant) { // check if variant was auto selected, then deselect - if (_.contains(autoSelectedVariants, variant.language.culture)) { + var autoSelected = autoSelectedVariants.find(function (x) { + return x.culture === variant.culture; + }); + if (autoSelected) { variant.save = false; } - ; variant.disabled = false; }); autoSelectedVariants = []; } + // disable submit button if nothing is selected + var firstSelected = vm.variants.find(function (v) { + return v.save; + }); + $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected + } + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return variant.language && variant.language.isMandatory === true && variant.segment == null; } function publishedVariantFilter(variant) { //determine a variant is 'published' (meaning it will show up as able unpublish) @@ -6968,18 +7726,11 @@ // * it has been published with pending changes return variant.state === 'Published' || variant.state === 'PublishedPendingChanges'; } - function unpublishedVariantFilter(variant) { - //determine a variant is 'modified' (meaning it will NOT show up as able to unpublish) - // * it's editor is in a $dirty state - // * it is published with pending changes - return variant.state !== 'Published' && variant.state !== 'PublishedPendingChanges'; - } //when this dialog is closed, remove all unpublish and disabled flags $scope.$on('$destroy', function () { - for (var i = 0; i < vm.variants.length; i++) { - vm.variants[i].save = false; - vm.variants[i].disabled = false; - } + vm.variants.forEach(function (variant) { + variant.save = variant.disabled = false; + }); }); onInit(); } @@ -7091,6 +7842,7 @@ $scope.getScaffoldMethod = getScaffold; //load the default culture selected in the main tree if any $scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + $scope.segment = $routeParams.csegment ? $routeParams.csegment : null; //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. //This is so we can listen to changes on the cculture parameter since that will not cause a route change //and then we can pass in the updated culture to the editor. @@ -7098,6 +7850,7 @@ //will not cause a route change and so we can update the isNew and contentId flags accordingly. $scope.$on('$routeUpdate', function (event, next) { $scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture; + $scope.segment = next.params.csegment ? next.params.csegment : null; $scope.isNew = $routeParams.id === '-1'; $scope.contentId = $routeParams.id; }); @@ -7191,6 +7944,7 @@ view: 'views/dashboard/content/overlays/delete.html', redirect: redirect, submitButtonLabelKey: 'contentTypeEditor_yesDelete', + submitButtonStyle: 'danger', submit: function submit(model) { performDelete(model.redirect); overlayService.close(); @@ -7340,7 +8094,7 @@ { 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', + img: 'views/dashboard/default/umbracotv.png', 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' @@ -7348,7 +8102,7 @@ { 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', + img: 'views/dashboard/default/ourumbraco.png', url: 'https://our.umbraco.com/?utm_source=core&utm_medium=dashboard&utm_content=image&utm_campaign=our', altText: 'Our Umbraco', buttonText: 'Visit Our Umbraco' @@ -7357,8 +8111,8 @@ }; evts.push(eventsService.on('appState.tour.complete', function (name, completedTour) { $timeout(function () { - angular.forEach(vm.tours, function (tourGroup) { - angular.forEach(tourGroup, function (tour) { + Utilities.forEach(vm.tours, function (tourGroup) { + Utilities.forEach(tourGroup, function (tour) { if (tour.alias === completedTour.alias) { tour.completed = true; } @@ -7488,7 +8242,7 @@ } angular.module('umbraco').controller('Umbraco.Dashboard.MediaFolderBrowserDashboardController', MediaFolderBrowserDashboardController); 'use strict'; - function ExamineManagementController($scope, $http, $q, $timeout, $location, umbRequestHelper, localizationService, overlayService, editorService) { + function ExamineManagementController($http, $q, $timeout, umbRequestHelper, localizationService, overlayService, editorService) { var vm = this; vm.indexerDetails = []; vm.searcherDetails = []; @@ -7623,17 +8377,17 @@ vm.searchResults.totalPages = Math.ceil(vm.searchResults.totalRecords / 20); // add URLs to edit well known entities _.each(vm.searchResults.results, function (result) { - var section = result.values['__IndexType']; + var section = result.values['__IndexType'][0]; switch (section) { case 'content': case 'media': - result.editUrl = '/' + section + '/' + section + '/edit/' + result.values['__NodeId']; - result.editId = result.values['__NodeId']; + result.editUrl = '/' + section + '/' + section + '/edit/' + result.values['__NodeId'][0]; + result.editId = result.values['__NodeId'][0]; result.editSection = section; break; case 'member': - result.editUrl = '/member/member/edit/' + result.values['__Key']; - result.editId = result.values['__Key']; + result.editUrl = '/member/member/edit/' + result.values['__Key'][0]; + result.editId = result.values['__Key'][0]; result.editSection = section; break; } @@ -7701,7 +8455,7 @@ 'use strict'; (function () { 'use strict'; - function HealthCheckController($scope, healthCheckResource) { + function HealthCheckController(healthCheckResource) { var SUCCESS = 0; var WARNING = 1; var ERROR = 2; @@ -7716,6 +8470,7 @@ vm.checkAllInGroup = checkAllInGroup; vm.openGroup = openGroup; vm.setViewState = setViewState; + vm.parseRegex = parseRegex; // Get a (grouped) list of all health checks healthCheckResource.getAllChecks().then(function (response) { vm.groups = response; @@ -7726,23 +8481,25 @@ 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; - } - }); + Utilities.forEach(group.checks, function (check) { + if (check.status) { + check.status.forEach(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; @@ -7776,19 +8533,21 @@ 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; - } + if (checks) { + checks.forEach(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; @@ -7803,6 +8562,9 @@ } } } + function parseRegex(regexAsString) { + return new RegExp(regexAsString); + } } angular.module('umbraco').controller('Umbraco.Dashboard.HealthCheckController', HealthCheckController); }()); @@ -7982,7 +8744,7 @@ $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, - formCtrl: this.createFolderForm + formCtrl: $scope.createFolderForm })) { dataTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { navigationService.hideMenu(); @@ -7993,8 +8755,16 @@ forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createFolderForm + }); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createFolderForm, + hasErrors: true + }); // TODO: Handle errors }); } ; @@ -8021,7 +8791,6 @@ */ function DataTypeDeleteController($scope, dataTypeResource, treeService, navigationService, localizationService) { var vm = this; - vm.propertyJoinSeparator = ', '; vm.hasReferences = false; vm.references = []; vm.performDelete = function () { @@ -8106,18 +8875,7 @@ vm.preValues = []; //method used to configure the pre-values when we retrieve them from the server function createPreValueProps(preVals) { - vm.preValues = []; - for (var i = 0; i < preVals.length; i++) { - vm.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 - }); - } + vm.preValues = dataTypeHelper.createPreValueProps(preVals); } function setHeaderNameState(content) { if (content.isSystem == 1) { @@ -8174,6 +8932,10 @@ vm.page.saveButtonState = 'success'; dataTypeHelper.rebindChangedProperties(vm.content, data); }, function (err) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); //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({ err: err }); @@ -8414,10 +9176,11 @@ var vm = this; vm.itemKey = ''; vm.createItem = createItem; + $scope.$emit('$changeTitle', ''); function createItem() { if (formHelper.submitForm({ scope: $scope, - formCtrl: this.createDictionaryForm + formCtrl: $scope.createDictionaryForm })) { var node = $scope.currentNode; dictionaryResource.create(node.id, vm.itemKey).then(function (data) { @@ -8431,11 +9194,19 @@ activate: true }); // reset form state - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createDictionaryForm + }); // navigate to edit view var currentSection = appState.getSectionState('currentSection'); $location.path('/' + currentSection + '/dictionary/edit/' + data); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createDictionaryForm, + hasErrors: true + }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); @@ -8511,8 +9282,10 @@ vm.page.menu.currentNode = null; vm.description = ''; vm.showBackButton = true; + vm.maxlength = 1000; vm.save = saveDictionary; vm.back = back; + vm.change = change; function loadDictionary() { vm.page.loading = true; //we are editing so get the content item from the server @@ -8535,6 +9308,7 @@ // create data for umb-property displaying for (var i = 0; i < data.translations.length; i++) { data.translations[i].property = createTranslationProperty(data.translations[i]); + change(data.translations[i]); } contentEditingHelper.handleSuccessfulSave({ scope: $scope, @@ -8562,13 +9336,14 @@ })) { vm.page.saveButtonState = 'busy'; dictionaryResource.save(vm.content, vm.nameDirty).then(function (data) { - formHelper.resetForm({ - scope: $scope, - notifications: data.notifications - }); + formHelper.resetForm({ scope: $scope }); bindDictionary(data); vm.page.saveButtonState = 'success'; }, function (err) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); contentEditingHelper.handleSaveError({ err: err }); notificationsService.error(err.data.message); vm.page.saveButtonState = 'error'; @@ -8578,6 +9353,12 @@ function back() { $location.path(vm.page.menu.currentSection + '/dictionary/list'); } + function change(translation) { + if (translation.translation) { + var charsCount = translation.translation.length; + translation.nearMaxLimit = charsCount > Math.max(vm.maxlength * 0.8, vm.maxlength - 50); + } + } $scope.$watch('vm.content.name', function (newVal, oldVal) { //when the value changes, we need to set the name dirty if (newVal && newVal !== oldVal && typeof oldVal !== 'undefined') { @@ -8605,7 +9386,7 @@ vm.loading = true; dictionaryResource.getList().then(function (data) { vm.items = data; - angular.forEach(vm.items, function (item) { + vm.items.forEach(function (item) { item.style = { 'paddingLeft': item.level * 10 }; }); vm.loading = false; @@ -8696,8 +9477,7 @@ $scope.model = { allowCreateFolder: $scope.currentNode.parentId === null || $scope.currentNode.nodeType === 'container', folderName: '', - creatingFolder: false, - creatingDoctypeCollection: false + creatingFolder: false }; var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; $scope.model.disableTemplates = disableTemplates; @@ -8705,15 +9485,10 @@ $scope.showCreateFolder = function () { $scope.model.creatingFolder = true; }; - $scope.showCreateDocTypeCollection = function () { - $scope.model.creatingDoctypeCollection = true; - $scope.model.collectionCreateTemplate = !$scope.model.disableTemplates; - $scope.model.collectionItemCreateTemplate = !$scope.model.disableTemplates; - }; $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, - formCtrl: this.createFolderForm + formCtrl: $scope.createFolderForm })) { contentTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { navigationService.hideMenu(); @@ -8724,47 +9499,18 @@ forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createFolderForm + }); var section = appState.getSectionState('currentSection'); }, function (err) { - $scope.error = err; - }); - } - }; - $scope.createCollection = function () { - if (formHelper.submitForm({ - scope: $scope, - formCtrl: this.createDoctypeCollectionForm, - statusMessage: 'Creating Doctype Collection...' - })) { - // see if we can find matching icons - var collectionIcon = 'icon-folders', collectionItemIcon = 'icon-document'; - iconHelper.getIcons().then(function (icons) { - for (var i = 0; i < icons.length; i++) { - // for matching we'll require a full match for collection, partial match for item - if (icons[i].substring(5) == $scope.model.collectionName.toLowerCase()) { - collectionIcon = icons[i]; - } else if (icons[i].substring(5).indexOf($scope.model.collectionItemName.toLowerCase()) > -1) { - collectionItemIcon = icons[i]; - } - } - contentTypeResource.createCollection(node.id, $scope.model.collectionName, $scope.model.collectionCreateTemplate, $scope.model.collectionItemName, $scope.model.collectionItemCreateTemplate, collectionIcon, collectionItemIcon).then(function (collectionData) { - navigationService.hideMenu(); - $location.search('create', null); - $location.search('notemplate', null); - formHelper.resetForm({ scope: $scope }); - var section = appState.getSectionState('currentSection'); - // redirect to the item id - $location.path('/' + section + '/documenttypes/edit/' + collectionData.containerId); - }, 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]); - } - } + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createFolderForm, + hasErrors: true }); + $scope.error = err; }); } }; @@ -8783,6 +9529,20 @@ $location.path('/settings/documenttypes/edit/' + node.id).search('create', 'true').search('notemplate', 'true'); navigationService.hideMenu(); }; + $scope.createComposition = function () { + $location.search('create', null); + $location.search('notemplate', null); + $location.search('iscomposition', null); + $location.path('/settings/documenttypes/edit/' + node.id).search('create', 'true').search('notemplate', 'true').search('iscomposition', 'true'); + navigationService.hideMenu(); + }; + $scope.createElement = function () { + $location.search('create', null); + $location.search('notemplate', null); + $location.search('iselement', null); + $location.path('/settings/documenttypes/edit/' + node.id).search('create', 'true').search('notemplate', 'true').search('iselement', 'true'); + navigationService.hideMenu(); + }; $scope.close = function () { var showMenu = true; navigationService.hideDialog(showMenu); @@ -8846,7 +9606,7 @@ */ (function () { 'use strict'; - function DocumentTypesEditController($scope, $routeParams, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $q, localizationService, overlayHelper, eventsService, angularHelper, editorService) { + function DocumentTypesEditController($scope, $routeParams, $q, contentTypeResource, editorState, contentEditingHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, localizationService, overlayHelper, eventsService, angularHelper, editorService) { var vm = this; var evts = []; var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; @@ -8856,6 +9616,7 @@ var isElement = $routeParams.iselement; var allowVaryByCulture = $routeParams.culturevary; var infiniteMode = $scope.model && $scope.model.infiniteMode; + var documentTypeIcon = ''; vm.save = save; vm.close = close; vm.currentNode = null; @@ -8877,9 +9638,10 @@ 'treeHeaders_templates', 'main_sections', 'shortcuts_navigateSections', + 'shortcuts_addTab', 'shortcuts_addGroup', 'shortcuts_addProperty', - 'shortcuts_addEditor', + 'defaultdialogs_selectEditor', 'shortcuts_editDataType', 'shortcuts_toggleListView', 'shortcuts_toggleAllowAsRoot', @@ -8911,41 +9673,16 @@ // keyboard shortcuts vm.labels.sections = values[4]; vm.labels.navigateSections = values[5]; - vm.labels.addGroup = values[6]; - vm.labels.addProperty = values[7]; - vm.labels.addEditor = values[8]; - vm.labels.editDataType = values[9]; - vm.labels.toggleListView = values[10]; - vm.labels.allowAsRoot = values[11]; - vm.labels.addChildNode = values[12]; - vm.labels.addTemplate = values[13]; - vm.labels.allowCultureVariants = values[14]; - var buttons = [ - { - 'name': vm.labels.design, - 'alias': 'design', - 'icon': 'icon-document-dashed-line', - 'view': 'views/documenttypes/views/design/design.html' - }, - { - 'name': vm.labels.listview, - 'alias': 'listView', - 'icon': 'icon-list', - 'view': 'views/documenttypes/views/listview/listview.html' - }, - { - 'name': vm.labels.permissions, - 'alias': 'permissions', - 'icon': 'icon-keychain', - 'view': 'views/documenttypes/views/permissions/permissions.html' - }, - { - 'name': vm.labels.templates, - 'alias': 'templates', - 'icon': 'icon-layout', - 'view': 'views/documenttypes/views/templates/templates.html' - } - ]; + vm.labels.addTab = values[6]; + vm.labels.addGroup = values[7]; + vm.labels.addProperty = values[8]; + vm.labels.addEditor = values[9]; + vm.labels.editDataType = values[10]; + vm.labels.toggleListView = values[11]; + vm.labels.allowAsRoot = values[12]; + vm.labels.addChildNode = values[13]; + vm.labels.addTemplate = values[14]; + vm.labels.allowCultureVariants = values[15]; vm.page.keyboardShortcutsOverview = [ { 'name': vm.labels.sections, @@ -8961,6 +9698,14 @@ { 'name': vm.labels.design, 'shortcuts': [ + { + 'description': vm.labels.addTab, + 'keys': [ + { 'key': 'alt' }, + { 'key': 'shift' }, + { 'key': 'a' } + ] + }, { 'description': vm.labels.addGroup, 'keys': [ @@ -9047,7 +9792,6 @@ }] } ]; - loadButtons(buttons); }); contentTypeHelper.checkModelsBuilderStatus().then(function (result) { vm.page.modelsBuilder = result; @@ -9059,7 +9803,6 @@ hotKeyWhenHidden: true, labelKey: vm.submitButtonKey, letter: 'S', - type: 'submit', handler: function handler() { vm.save(); } @@ -9129,13 +9872,15 @@ vm.page.loading = false; }); } - function loadButtons(buttons) { - angular.forEach(buttons, function (val, index) { - if (disableTemplates === true && val.alias === 'templates') { - buttons.splice(index, 1); - } - }); - vm.page.navigation = buttons; + function loadButtons() { + vm.page.navigation = vm.contentType.apps; + if (disableTemplates === true) { + Utilities.forEach(vm.contentType.apps, function (app, index) { + if (app.alias === 'templates') { + vm.page.navigation.splice(index, 1); + } + }); + } initializeActiveNavigationPanel(); } function initializeActiveNavigationPanel() { @@ -9144,9 +9889,8 @@ var initialViewSetFromRouteParams = false; var view = $routeParams.view; if (view) { - var viewPath = 'views/documenttypes/views/' + view + '/' + view + '.html'; for (var i = 0; i < vm.page.navigation.length; i++) { - if (vm.page.navigation[i].view === viewPath) { + if (vm.page.navigation[i].alias.localeCompare(view, undefined, { sensitivity: 'accent' }) === 0) { vm.page.navigation[i].active = true; initialViewSetFromRouteParams = true; break; @@ -9159,7 +9903,7 @@ } /* ---------- SAVE ---------- */ function save() { - saveInternal().then(angular.noop, angular.noop); + saveInternal().then(Utilities.noop, Utilities.noop); } /** This internal save method performs the actual saving and returns a promise, not to be bound to any buttons but used by other bound methods */ function saveInternal() { @@ -9175,40 +9919,13 @@ scope: $scope, content: vm.contentType, infiniteMode: infiniteMode, - // we need to rebind... the IDs that have been created! - rebindCallback: function rebindCallback(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; - }); - }); + rebindCallback: function rebindCallback(_, savedContentType) { + // we need to rebind... the IDs that have been created! + contentTypeHelper.rebindSavedContentType(vm.contentType, savedContentType); } }).then(function (data) { + // allow UI to access server validation state + vm.contentType.ModelState = data.ModelState; //success // we don't need to sync the tree in infinite mode if (!infiniteMode) { @@ -9217,15 +9934,21 @@ // emit event var args = { documentType: vm.contentType }; eventsService.emit('editors.documentType.saved', args); + if (documentTypeIcon !== vm.contentType.icon) { + eventsService.emit('editors.tree.icon.changed', args); + } vm.page.saveButtonState = 'success'; if (infiniteMode && $scope.model.submit) { $scope.model.documentTypeAlias = vm.contentType.alias; + $scope.model.documentTypeKey = vm.contentType.key; $scope.model.submit($scope.model); } return $q.resolve(data); }, function (err) { //error if (err) { + // allow UI to access server validation state + vm.contentType.ModelState = err.data.ModelState; editorState.set($scope.content); } else { localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { @@ -9242,15 +9965,6 @@ } } 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 (!noTemplate && contentType.id === 0) { contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); @@ -9269,6 +9983,8 @@ //set a shared state editorState.set(contentType); vm.contentType = contentType; + documentTypeIcon = contentType.icon; + loadButtons(); } /** Syncs the template alias for new doc types before saving if a template is to be created */ function syncTemplateAlias(contentType) { @@ -9279,7 +9995,7 @@ contentType.defaultTemplate.alias = contentType.alias; } //sync allowed templates that had the placeholder flag - angular.forEach(contentType.allowedTemplates, function (allowedTemplate) { + contentType.allowedTemplates.forEach(function (allowedTemplate) { if (allowedTemplate.placeholder) { allowedTemplate.name = contentType.name; allowedTemplate.alias = contentType.alias; @@ -9297,14 +10013,6 @@ // 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) { var args = { @@ -9546,23 +10254,20 @@ function PermissionsController($scope, $timeout, contentTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { /* ----------- SCOPE VARIABLES ----------- */ var vm = this; - var childNodeSelectorOverlayTitle = ''; vm.contentTypes = []; vm.selectedChildren = []; - vm.overlayTitle = ''; + vm.showAllowSegmentationOption = Umbraco.Sys.ServerVariables.umbracoSettings.showAllowSegmentationForDocumentTypes || false; vm.addChild = addChild; vm.removeChild = removeChild; vm.sortChildren = sortChildren; vm.toggleAllowAsRoot = toggleAllowAsRoot; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.toggleAllowSegmentVariants = toggleAllowSegmentVariants; vm.canToggleIsElement = false; vm.toggleIsElement = toggleIsElement; /* ---------- INIT ---------- */ init(); function init() { - localizationService.localize('contentTypeEditor_chooseChildNode').then(function (value) { - childNodeSelectorOverlayTitle = value; - }); contentTypeResource.getAll().then(function (contentTypes) { vm.contentTypes = _.where(contentTypes, { isElement: false }); // convert legacy icons @@ -9582,23 +10287,27 @@ } } function addChild($event) { - var childNodeSelectorOverlay = { + var dialog = { view: 'itempicker', - title: childNodeSelectorOverlayTitle, availableItems: vm.contentTypes, selectedItems: vm.selectedChildren, position: 'target', event: $event, submit: function submit(model) { - vm.selectedChildren.push(model.selectedItem); - $scope.model.allowedContentTypes.push(model.selectedItem.id); + if (model.selectedItem) { + vm.selectedChildren.push(model.selectedItem); + $scope.model.allowedContentTypes.push(model.selectedItem.id); + } overlayService.close(); }, close: function close() { overlayService.close(); } }; - overlayService.open(childNodeSelectorOverlay); + localizationService.localize('contentTypeEditor_chooseChildNode').then(function (value) { + dialog.title = value; + overlayService.open(dialog); + }); } function removeChild(selectedChild, index) { // remove from vm @@ -9620,6 +10329,9 @@ function toggleAllowCultureVariants() { $scope.model.allowCultureVariant = $scope.model.allowCultureVariant ? false : true; } + function toggleAllowSegmentVariants() { + $scope.model.allowSegmentVariant = $scope.model.allowSegmentVariant ? false : true; + } function toggleIsElement() { $scope.model.isElement = $scope.model.isElement ? false : true; } @@ -9697,6 +10409,44 @@ angular.module('umbraco').controller('Umbraco.Editors.DocumentType.TemplatesController', TemplatesController); }()); 'use strict'; + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); + } + function _nonIterableRest() { + throw new TypeError('Invalid attempt to destructure non-iterable instance'); + } + function _iterableToArrayLimit(arr, i) { + if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === '[object Arguments]')) { + return; + } + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + if (i && _arr.length === i) + break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i['return'] != null) + _i['return'](); + } finally { + if (_d) + throw _e; + } + } + return _arr; + } + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) + return arr; + } (function () { 'use strict'; function LanguagesEditController($scope, $q, $timeout, $location, $routeParams, overlayService, navigationService, notificationsService, localizationService, languageResource, contentEditingHelper, formHelper, eventsService) { @@ -9727,7 +10477,8 @@ 'languages_noFallbackLanguageOption', 'languages_fallbackLanguageDescription', 'languages_fallbackLanguage', - 'defaultdialogs_confirmSure' + 'defaultdialogs_confirmSure', + 'defaultdialogs_editlanguage' ]; localizationService.localizeMany(labelKeys).then(function (values) { vm.labels.languages = values[0]; @@ -9738,6 +10489,7 @@ vm.labels.addLanguage = values[5]; vm.labels.noFallbackLanguageOption = values[6]; vm.labels.areYouSure = values[9]; + vm.labels.editLanguage = values[10]; $scope.properties = { fallbackLanguage: { alias: 'fallbackLanguage', @@ -9747,6 +10499,7 @@ }; if ($routeParams.create) { vm.page.name = vm.labels.addLanguage; + $scope.$emit('$changeTitle', vm.labels.addLanguage); } }); vm.loading = true; @@ -9754,7 +10507,8 @@ //load all culture/languages promises.push(languageResource.getCultures().then(function (culturesDictionary) { var cultures = []; - angular.forEach(culturesDictionary, function (value, key) { + Object.entries(culturesDictionary).forEach(function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), key = _ref2[0], value = _ref2[1]; cultures.push({ name: key, displayName: value @@ -9773,9 +10527,10 @@ promises.push(languageResource.getById($routeParams.id).then(function (lang) { vm.language = lang; vm.page.name = vm.language.name; + $scope.$emit('$changeTitle', vm.labels.editLanguage + ': ' + vm.page.name); /* we need to store the initial default state so we can disable the toggle if it is the default. we need to prevent from not having a default language. */ - vm.initIsDefault = angular.copy(vm.language.isDefault); + vm.initIsDefault = Utilities.copy(vm.language.isDefault); makeBreadcrumbs(); //store to check if we are changing the lang culture currCulture = vm.language.culture; @@ -9832,6 +10587,10 @@ back(); }, function (err) { vm.page.saveButtonState = 'error'; + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); formHelper.handleError(err); }); } @@ -9872,7 +10631,7 @@ 'use strict'; (function () { 'use strict'; - function LanguagesOverviewController($location, $timeout, navigationService, localizationService, languageResource, eventsService, overlayService) { + function LanguagesOverviewController($location, $timeout, navigationService, localizationService, languageResource, eventsService, overlayService, $scope) { var vm = this; vm.page = {}; vm.languages = []; @@ -9904,6 +10663,7 @@ vm.labels.fallsbackTo = values[3]; // set page name vm.page.name = vm.labels.languages; + $scope.$emit('$changeTitle', vm.labels.languages); }); languageResource.getAll().then(function (languages) { vm.languages = languages; @@ -10012,6 +10772,14 @@ vm.commonLogMessages = []; vm.commonLogMessagesCount = 10; vm.dateRangeLabel = ''; + vm.config = { + enableTime: false, + dateFormat: 'Y-m-d', + time_24hr: false, + mode: 'range', + maxDate: 'today', + conjunction: ' to ' + }; // ChartJS Options - for count/overview of log distribution vm.logTypeLabels = [ 'Debug', @@ -10028,11 +10796,11 @@ 0 ]; vm.logTypeColors = [ - '#eaddd5', + '#2e8aea', '#2bc37c', - '#3544b1', '#ff9412', - '#d42054' + '#d42054', + '#343434' ]; vm.chartOptions = { legend: { @@ -10040,6 +10808,12 @@ position: 'left' } }; + // Functions + vm.searchLogQuery = searchLogQuery; + vm.findMessageTemplate = findMessageTemplate; + vm.searchErrors = searchErrors; + vm.showMore = showMore; + vm.dateRangeChange = dateRangeChange; var querystring = $location.search(); if (querystring.startDate) { vm.startDate = querystring.startDate; @@ -10062,20 +10836,15 @@ vm.startDate, vm.endDate ]; - //functions - vm.searchLogQuery = searchLogQuery; - vm.findMessageTemplate = findMessageTemplate; - vm.searchErrors = searchErrors; - vm.showMore = showMore; function preFlightCheck() { vm.loading = true; - //Do our pre-flight check (to see if we can view logs) - //IE the log file is NOT too big such as 1GB & crash the site + // Do our pre-flight check (to see if we can view logs) + // IE the log file is NOT too big such as 1GB & crash the site logViewerResource.canViewLogs(vm.startDate, vm.endDate).then(function (result) { vm.loading = false; vm.canLoadLogs = result; if (result) { - //Can view logs - so initalise + // Can view logs - so initialize init(); } }); @@ -10087,7 +10856,7 @@ vm.loading = true; var savedSearches = logViewerResource.getSavedSearches().then(function (data) { vm.searches = data; - }, // fallback to some defaults if error from API response + }, // Fallback to some defaults if error from API response function () { vm.searches = [ { @@ -10144,7 +10913,7 @@ }); vm.logLevelColor = index > -1 ? vm.logTypeColors[index] : '#000'; }); - //Set loading indicator to false when these 3 queries complete + // Set loading indicator to false when these 3 queries complete $q.all([ savedSearches, numOfErrors, @@ -10161,6 +10930,7 @@ }); }); } + preFlightCheck(); function searchLogQuery(logQuery) { $location.path('/settings/logViewer/search').search({ lq: logQuery, @@ -10179,17 +10949,7 @@ var logQuery = '@Level=\'Fatal\' or @Level=\'Error\' or Has(@Exception)'; searchLogQuery(logQuery); } - preFlightCheck(); - ///////////////////// - vm.config = { - enableTime: false, - dateFormat: 'Y-m-d', - time_24hr: false, - mode: 'range', - maxDate: 'today', - conjunction: ' to ' - }; - vm.dateRangeChange = function (selectedDates, dateStr, instance) { + function dateRangeChange(selectedDates, dateStr, instance) { if (selectedDates.length > 0) { // Update view by re-requesting route with updated querystring. // By doing this we make sure the URL matches the selected time period, aiding sharing the link. @@ -10198,19 +10958,24 @@ var startDate = selectedDates[0].toIsoDateString(); var endDate = selectedDates[selectedDates.length - 1].toIsoDateString(); // Take the last date as end + // Check if date range has changed + if (startDate === vm.period[0] && endDate === vm.period[1]) { + // Same date range + return; + } $location.path('/settings/logViewer/overview').search({ startDate: startDate, endDate: endDate }); } - }; + } } angular.module('umbraco').controller('Umbraco.Editors.LogViewer.OverviewController', LogViewerOverviewController); }()); 'use strict'; (function () { 'use strict'; - function LogViewerSearchController($location, logViewerResource, overlayService) { + function LogViewerSearchController($location, $timeout, logViewerResource, overlayService, localizationService) { var vm = this; vm.loading = false; vm.logsLoading = false; @@ -10221,11 +10986,11 @@ vm.logLevels = [ { name: 'Verbose', - logTypeColor: '' + logTypeColor: 'gray' }, { name: 'Debug', - logTypeColor: 'gray' + logTypeColor: 'info' }, { name: 'Information', @@ -10233,17 +10998,85 @@ }, { name: 'Warning', - logTypeColor: 'primary' + logTypeColor: 'warning' }, { name: 'Error', - logTypeColor: 'warning' + logTypeColor: 'danger' }, { name: 'Fatal', - logTypeColor: 'danger' + logTypeColor: 'dark' } ]; + vm.polling = { + enabled: false, + interval: 0, + promise: null, + defaultButton: { + labelKey: 'logViewer_polling', + handler: function handler() { + if (vm.polling.enabled) { + vm.polling.enabled = false; + vm.polling.interval = 0; + vm.polling.defaultButton.icon = null; + vm.polling.defaultButton.labelKey = 'logViewer_polling'; + } else { + vm.polling.subButtons[0].handler(); + } + } + }, + subButtons: [ + { + labelKey: 'logViewer_every2', + handler: function handler() { + enablePolling(2); + } + }, + { + labelKey: 'logViewer_every5', + handler: function handler() { + enablePolling(5); + } + }, + { + labelKey: 'logViewer_every10', + handler: function handler() { + enablePolling(10); + } + }, + { + labelKey: 'logViewer_every20', + handler: function handler() { + enablePolling(20); + } + }, + { + labelKey: 'logViewer_every30', + handler: function handler() { + enablePolling(30); + } + } + ] + }; + function enablePolling(interval) { + vm.polling.enabled = true; + vm.polling.interval = interval; + vm.polling.defaultButton.icon = 'icon-axis-rotation fa-spin'; + vm.polling.defaultButton.labelKey = 'logViewer_pollingEvery' + interval; + if (vm.polling.promise) { + $timeout.cancel(vm.polling.promise); + } + vm.polling.promise = poll(interval); + } + function poll(interval) { + vm.polling.promise = $timeout(function () { + getLogs(true, true); + if (vm.polling.enabled && vm.polling.interval > 0) { + poll(vm.polling.interval); + } + }, interval * 1000); + } vm.searches = []; vm.logItems = {}; vm.logOptions = {}; @@ -10279,6 +11112,8 @@ vm.search = search; vm.getFilterName = getFilterName; vm.setLogLevelFilter = setLogLevelFilter; + vm.selectAllLogLevelFilters = selectAllLogLevelFilters; + vm.deselectAllLogLevelFilters = deselectAllLogLevelFilters; vm.toggleOrderBy = toggleOrderBy; vm.selectSearch = selectSearch; vm.resetSearch = resetSearch; @@ -10347,9 +11182,20 @@ vm.logOptions.pageNumber = pageNumber; getLogs(); } - function getLogs() { - vm.logsLoading = true; + function getLogs(hideLoadingIndicator, keepOpenItems) { + vm.logsLoading = !hideLoadingIndicator; logViewerResource.getLogs(vm.logOptions).then(function (data) { + if (keepOpenItems) { + var openItemTimestamps = vm.logItems.items.filter(function (item) { + return item.open; + }).map(function (item) { + return item.Timestamp; + }); + data.items = data.items.map(function (item) { + item.open = openItemTimestamps.indexOf(item.Timestamp) > -1; + return item; + }); + } vm.logItems = data; vm.logsLoading = false; setLogTypeColor(vm.logItems.items); @@ -10367,7 +11213,7 @@ function getFilterName(array) { var name = 'All'; var found = false; - angular.forEach(array, function (item) { + array.forEach(function (item) { if (item.selected) { if (!found) { name = item.name; @@ -10391,6 +11237,23 @@ } getLogs(); } + function updateAllLogLevelFilterCheckboxes(bool) { + vm.logLevels.forEach(function (logLevel) { + return logLevel.selected = bool; + }); + } + function selectAllLogLevelFilters() { + vm.logOptions.logLevels = vm.logLevels.map(function (logLevel) { + return logLevel.name; + }); + updateAllLogLevelFilterCheckboxes(true); + getLogs(); + } + function deselectAllLogLevelFilters() { + vm.logOptions.logLevels = []; + updateAllLogLevelFilterCheckboxes(false); + getLogs(); + } function toggleOrderBy() { vm.logOptions.orderDirection = vm.logOptions.orderDirection === 'Descending' ? 'Ascending' : 'Descending'; getLogs(); @@ -10427,7 +11290,6 @@ function addToSavedSearches() { var overlay = { title: 'Save Search', - subtitle: 'Enter a friendly name for your search query', closeButtonLabel: 'Cancel', submitButtonLabel: 'Save Search', disableSubmitButton: true, @@ -10436,25 +11298,31 @@ submit: function submit(model) { //Resource call with two params (name & query) //API that opens the JSON and adds it to the bottom - logViewerResource.postSavedSearch(model.name, model.query).then(function (data) { + logViewerResource.postSavedSearch(model.queryName, model.query).then(function (data) { vm.searches = data; overlayService.close(); }); }, close: function close() { - overlayService.close(); + return overlayService.close(); } }; - overlayService.open(overlay); + var labelKeys = [ + 'general_cancel', + 'logViewer_saveSearch', + 'logViewer_saveSearchDescription' + ]; + localizationService.localizeMany(labelKeys).then(function (values) { + overlay.title = values[1]; + overlay.subtitle = values[2], overlay.submitButtonLabel = values[1], overlay.closeButtonLabel = values[0], overlayService.open(overlay); + }); } function deleteSavedSearch(searchItem) { var overlay = { title: 'Delete Saved Search', - subtitle: 'Are you sure you wish to delete?', closeButtonLabel: 'Cancel', submitButtonLabel: 'Delete Saved Search', submitButtonStyle: 'danger', - view: 'default', submit: function submit(model) { //Resource call with two params (name & query) //API that opens the JSON and adds it to the bottom @@ -10464,10 +11332,21 @@ }); }, close: function close() { - overlayService.close(); + return overlayService.close(); } }; - overlayService.open(overlay); + var labelKeys = [ + 'general_cancel', + 'defaultdialogs_confirmdelete', + 'logViewer_deleteSavedSearch' + ]; + localizationService.localizeMany(labelKeys).then(function (values) { + overlay.title = values[2]; + overlay.subtitle = values[1]; + overlay.submitButtonLabel = values[2]; + overlay.closeButtonLabel = values[0]; + overlayService.open(overlay); + }); } function back() { $location.path('settings/logViewer/overview').search('lq', null); @@ -10503,7 +11382,6 @@ editorService.open(overlay); } function submit() { - console.log('model', $scope.model); if ($scope.model && $scope.model.submit && formHelper.submitForm({ scope: $scope })) { $scope.model.submit($scope.model); } @@ -10531,7 +11409,7 @@ function createItem() { if (formHelper.submitForm({ scope: $scope, - formCtrl: this.createMacroForm + formCtrl: $scope.createMacroForm })) { var node = $scope.currentNode; macroResource.createMacro(vm.itemKey).then(function (data) { @@ -10545,11 +11423,19 @@ activate: true }); // reset form state - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createMacroForm + }); // navigate to edit view var currentSection = appState.getSectionState('currentSection'); $location.path('/' + currentSection + '/macros/edit/' + data); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createMacroForm, + hasErrors: true + }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); @@ -10618,13 +11504,14 @@ })) { vm.page.saveButtonState = 'busy'; macroResource.saveMacro(vm.macro).then(function (data) { - formHelper.resetForm({ - scope: $scope, - notifications: data.notifications - }); + formHelper.resetForm({ scope: $scope }); bindMacro(data); vm.page.saveButtonState = 'success'; }, function (error) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); contentEditingHelper.handleSaveError({ err: error }); vm.page.saveButtonState = 'error'; }); @@ -10876,8 +11763,30 @@ 'use strict'; (function () { 'use strict'; - function MediaAppContentController($scope) { + function MediaAppContentController($scope, $filter, contentEditingHelper, contentTypeHelper) { var vm = this; + vm.tabs = []; + vm.activeTabAlias = null; + vm.setActiveTab = setActiveTab; + $scope.$watchCollection('content.tabs', function (newValue) { + contentTypeHelper.defineParentAliasOnGroups(newValue); + contentTypeHelper.relocateDisorientedGroups(newValue); + vm.tabs = $filter('filter')(newValue, function (tab) { + return tab.type === 1; + }); + if (vm.tabs.length > 0) { + // if we have tabs and some groups that doesn't belong to a tab we need to render those on an "Other" tab. + contentEditingHelper.registerGenericTab(newValue); + setActiveTab(vm.tabs[0]); + } + }); + function setActiveTab(tab) { + vm.activeTabAlias = tab.alias; + vm.tabs.forEach(function (tab) { + return tab.active = false; + }); + tab.active = true; + } } angular.module('umbraco').controller('Umbraco.Editors.Media.Apps.ContentController', MediaAppContentController); }()); @@ -10994,23 +11903,23 @@ * @ngdoc controller * @name Umbraco.Editors.Media.EditController * @function - * + * * @description * The controller for the media editor */ - function mediaEditController($scope, $routeParams, $q, appState, mediaResource, entityResource, navigationService, notificationsService, localizationService, serverValidationManager, contentEditingHelper, fileManager, formHelper, editorState, umbRequestHelper, $http, eventsService, $location) { + function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource, entityResource, navigationService, notificationsService, localizationService, serverValidationManager, contentEditingHelper, fileManager, formHelper, editorState, umbRequestHelper, eventsService) { var evts = []; var nodeId = null; var create = false; var infiniteMode = $scope.model && $scope.model.infiniteMode; - // when opening the editor through infinite editing get the + // when opening the editor through infinite editing get the // node id from the model instead of the route param if (infiniteMode && $scope.model.id) { nodeId = $scope.model.id; } else { nodeId = $routeParams.id; } - // when opening the editor through infinite editing get the + // when opening the editor through infinite editing get the // create option from the model instead of the route param if (infiniteMode) { create = $scope.model.create; @@ -11039,28 +11948,32 @@ $scope.content = data; init(); $scope.page.loading = false; + }, function () { + $scope.page.loading = false; }); } else { $scope.page.loading = true; loadMedia().then(function () { $scope.page.loading = false; + }, function () { + $scope.page.loading = false; }); } function init() { var content = $scope.content; - // we need to check wether an app is present in the current data, if not we will present the default app. + // we need to check whether an app is present in the current data, if not we will present the default app. var isAppPresent = false; // on first init, we dont have any apps. but if we are re-initializing, we do, but ... if ($scope.app) { // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) - _.forEach(content.apps, function (app) { + content.apps.forEach(function (app) { if (app === $scope.app) { isAppPresent = true; } }); // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { - _.forEach(content.apps, function (app) { + content.apps.forEach(function (app) { if (app.alias === $scope.app.alias) { isAppPresent = true; app.active = true; @@ -11088,6 +12001,8 @@ $scope.page.loading = true; loadMedia().then(function () { $scope.page.loading = false; + }, function () { + $scope.page.loading = false; }); } })); @@ -11113,7 +12028,7 @@ 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 + //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; @@ -11136,21 +12051,30 @@ $scope.page.saveButtonState = 'busy'; mediaResource.save($scope.content, create, fileManager.getFiles()).then(function (data) { formHelper.resetForm({ scope: $scope }); - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - editorState.set($scope.content); - syncTreeNode($scope.content, data.path); - init(); - $scope.page.saveButtonState = 'success'; // close the editor if it's infinite mode + // submit function manages rebinding changes if (infiniteMode && $scope.model.submit) { $scope.model.mediaNode = $scope.content; $scope.model.submit($scope.model); + } else { + // if not infinite mode, rebind changed props etc + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + editorState.set($scope.content); + syncTreeNode($scope.content, data.path); + $scope.page.saveButtonState = 'success'; + init(); } + eventsService.emit('editors.media.saved', { media: data }); + return data; }, function (err) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); contentEditingHelper.handleSaveError({ err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) @@ -11177,7 +12101,7 @@ if (!infiniteMode) { syncTreeNode($scope.content, data.path, true); } - if ($scope.content.parentId && $scope.content.parentId != -1) { + if ($scope.content.parentId && $scope.content.parentId !== -1 && $scope.content.parentId !== -21) { //We fetch all ancestors of the node to generate the footer breadcrump navigation entityResource.getAncestors(nodeId, 'media').then(function (anc) { $scope.ancestors = anc; @@ -11186,6 +12110,9 @@ init(); $scope.page.loading = false; $q.resolve($scope.content); + }, function (error) { + $scope.page.loading = false; + $q.reject(error); }); } $scope.close = function () { @@ -11350,6 +12277,7 @@ } }); }, function (err) { + $scope.busy = false; $scope.success = false; $scope.error = err; }); @@ -11553,7 +12481,7 @@ 'use strict'; (function () { 'use strict'; - function MediaSortController($scope, $filter, mediaResource, navigationService) { + function MediaSortController($scope, $filter, mediaResource, navigationService, eventsService) { var vm = this; var id = $scope.currentNode.id; vm.loading = false; @@ -11561,6 +12489,8 @@ vm.saveButtonState = 'init'; vm.sortOrder = {}; vm.sortableOptions = { + axis: 'y', + containment: 'parent', distance: 10, tolerance: 'pointer', opacity: 0.7, @@ -11599,6 +12529,7 @@ }).then(function () { return navigationService.reloadNode($scope.currentNode); }); + eventsService.emit('sortCompleted', { id: id }); vm.saveButtonState = 'success'; }, function (error) { vm.error = error; @@ -11708,7 +12639,7 @@ $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, - formCtrl: this.createFolderForm + formCtrl: $scope.createFolderForm })) { mediaTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { navigationService.hideMenu(); @@ -11719,9 +12650,17 @@ forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createFolderForm + }); var section = appState.getSectionState('currentSection'); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createFolderForm, + hasErrors: true + }); $scope.error = err; }); } @@ -11795,12 +12734,13 @@ */ (function () { 'use strict'; - function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $q, localizationService, overlayHelper, eventsService, angularHelper) { + function MediaTypesEditController($scope, $routeParams, $q, mediaTypeResource, editorState, contentEditingHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, localizationService, overlayHelper, eventsService, angularHelper) { var vm = this; var evts = []; var mediaTypeId = $routeParams.id; var create = $routeParams.create; var infiniteMode = $scope.model && $scope.model.infiniteMode; + var mediaTypeIcon = ''; vm.save = save; vm.close = close; vm.currentNode = null; @@ -11985,7 +12925,6 @@ hotKeyWhenHidden: true, labelKey: vm.saveButtonKey, letter: 'S', - type: 'submit', handler: function handler() { vm.save(); } @@ -12064,38 +13003,9 @@ saveMethod: mediaTypeResource.save, scope: $scope, content: vm.contentType, - // we need to rebind... the IDs that have been created! - rebindCallback: function rebindCallback(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; - }); - }); + rebindCallback: function rebindCallback(_, savedContentType) { + // we need to rebind... the IDs that have been created! + contentTypeHelper.rebindSavedContentType(vm.contentType, savedContentType); } }).then(function (data) { //success @@ -12105,6 +13015,9 @@ // emit event var args = { mediaType: vm.contentType }; eventsService.emit('editors.mediaType.saved', args); + if (mediaTypeIcon !== vm.contentType.icon) { + eventsService.emit('editors.tree.icon.changed', args); + } vm.page.saveButtonState = 'success'; if (infiniteMode && $scope.model.submit) { $scope.model.submit(); @@ -12128,20 +13041,12 @@ } } 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; + mediaTypeIcon = contentType.icon; } function convertLegacyIcons(contentType) { // make array to store contentType icon @@ -12153,14 +13058,6 @@ // 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({ @@ -12266,7 +13163,6 @@ function PermissionsController($scope, $timeout, mediaTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { /* ----------- SCOPE VARIABLES ----------- */ var vm = this; - var childNodeSelectorOverlayTitle = ''; vm.mediaTypes = []; vm.selectedChildren = []; vm.addChild = addChild; @@ -12276,9 +13172,6 @@ /* ---------- INIT ---------- */ init(); function init() { - localizationService.localize('contentTypeEditor_chooseChildNode').then(function (value) { - childNodeSelectorOverlayTitle = value; - }); mediaTypeResource.getAll().then(function (mediaTypes) { vm.mediaTypes = mediaTypes; // convert legacy icons @@ -12290,23 +13183,27 @@ }); } function addChild($event) { - var childNodeSelectorOverlay = { + var dialog = { view: 'itempicker', - title: childNodeSelectorOverlayTitle, availableItems: vm.mediaTypes, selectedItems: vm.selectedChildren, position: 'target', event: $event, submit: function submit(model) { - vm.selectedChildren.push(model.selectedItem); - $scope.model.allowedContentTypes.push(model.selectedItem.id); + if (model.selectedItem) { + vm.selectedChildren.push(model.selectedItem); + $scope.model.allowedContentTypes.push(model.selectedItem.id); + } overlayService.close(); }, close: function close() { overlayService.close(); } }; - overlayService.open(childNodeSelectorOverlay); + localizationService.localize('contentTypeEditor_chooseChildNode').then(function (value) { + dialog.title = value; + overlayService.open(dialog); + }); } function removeChild(selectedChild, index) { // remove from vm @@ -12337,8 +13234,39 @@ 'use strict'; (function () { 'use strict'; - function MemberAppContentController($scope) { + function MemberAppContentController($scope, $filter, contentEditingHelper, contentTypeHelper) { var vm = this; + vm.tabs = []; + vm.activeTabAlias = null; + vm.setActiveTab = setActiveTab; + vm.hideSystemProperties = hideSystemProperties; + $scope.$watchCollection('content.tabs', function (newValue) { + contentTypeHelper.defineParentAliasOnGroups(newValue); + contentTypeHelper.relocateDisorientedGroups(newValue); + vm.tabs = $filter('filter')(newValue, function (tab) { + return tab.type === 1; + }); + if (vm.tabs.length > 0) { + // if we have tabs and some groups that doesn't belong to a tab we need to render those on an "Other" tab. + contentEditingHelper.registerGenericTab(newValue); + setActiveTab(vm.tabs[0]); + } + }); + function setActiveTab(tab) { + vm.activeTabAlias = tab.alias; + vm.tabs.forEach(function (tab) { + return tab.active = false; + }); + tab.active = true; + } + function hideSystemProperties(property) { + // hide some specific, known properties by alias + if (property.alias === '_umb_id' || property.alias === '_umb_doctype') { + return false; + } + // hide all label properties with the alias prefix "umbracoMember" (e.g. "umbracoMemberFailedPasswordAttempts") + return property.view !== 'readonlyvalue' || property.alias.startsWith('umbracoMember') === false; + } } angular.module('umbraco').controller('Umbraco.Editors.Member.Apps.ContentController', MemberAppContentController); }()); @@ -12402,7 +13330,8 @@ * @description * The controller for the member editor */ - function MemberEditController($scope, $routeParams, $location, appState, memberResource, entityResource, navigationService, notificationsService, localizationService, serverValidationManager, contentEditingHelper, fileManager, formHelper, editorState, umbRequestHelper, $http) { + function MemberEditController($scope, $routeParams, $location, $http, $q, appState, memberResource, entityResource, navigationService, notificationsService, localizationService, serverValidationManager, contentEditingHelper, fileManager, formHelper, editorState, umbRequestHelper, eventsService) { + var evts = []; var infiniteMode = $scope.model && $scope.model.infiniteMode; var id = infiniteMode ? $scope.model.id : $routeParams.id; var create = infiniteMode ? $scope.model.create : $routeParams.create; @@ -12443,41 +13372,10 @@ }); } } 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 just in case. - //(isNumber doesnt work here since its seen as a string) - //The reason this might be an INT is due to the routing used for the member list view - //but this is now configured to use the key, so this is just a fail safe - if (id && id.length < 9) { - entityResource.getById(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(id).then(function (data) { - $scope.content = data; - init(); - if (!infiniteMode) { - 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.notifyAndClearAllSubscriptions(); - $scope.page.loading = false; - }); - } + $scope.page.loading = true; + loadMember().then(function () { + $scope.page.loading = false; + }); } function init() { var content = $scope.content; @@ -12511,6 +13409,49 @@ $scope.page.nameLocked = true; } editorState.set($scope.content); + bindEvents(); + } + function bindEvents() { + //bindEvents can be called more than once and we don't want to have multiple bound events + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + evts.push(eventsService.on('editors.memberType.saved', function (name, args) { + // if this member item uses the updated member type we need to reload the member item + if (args && args.memberType && args.memberType.key.replace(/-/g, '') === $scope.content.contentType.key) { + $scope.page.loading = true; + loadMember().then(function () { + $scope.page.loading = false; + }); + } + })); + } + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(content, path, initialLoad) { + if (infiniteMode) { + return; + } + if (!$scope.content.isChildOfListView) { + navigationService.syncTree({ + tree: 'member', + 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: 'member', + 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; + }); + } } /** Just shows a simple notification that there are client side validation issues to be fixed */ function showValidationNotification() { @@ -12536,23 +13477,35 @@ } memberResource.save($scope.content, create, fileManager.getFiles()).then(function (data) { formHelper.resetForm({ scope: $scope }); - contentEditingHelper.handleSuccessfulSave({ + // close the editor if it's infinite mode + // submit function manages rebinding changes + if (infiniteMode && $scope.model.submit) { + $scope.model.memberNode = $scope.content; + $scope.model.submit($scope.model); + } else { + // if not infinite mode, rebind changed props etc + 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); + var path = buildTreePath(data); + navigationService.syncTree({ + tree: 'member', + path: path.split(',') + }); + //syncTreeNode($scope.content, data.path); + $scope.page.saveButtonState = 'success'; + init(); + } + }, function (err) { + formHelper.resetForm({ 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.page.saveButtonState = 'success'; - var path = buildTreePath(data); - //sync the tree (only for ui purposes) - navigationService.syncTree({ - tree: 'member', - path: path.split(','), - forceReload: true + hasErrors: true }); - }, function (err) { contentEditingHelper.handleSaveError({ err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) @@ -12564,6 +13517,51 @@ showValidationNotification(); } }; + function loadMember() { + var deferred = $q.defer(); + //so, we usually reference all editors with the Int ID, but with members we have + //a different pattern, adding a route-redirect here to handle this just in case. + //(isNumber doesnt work here since its seen as a string) + //The reason this might be an INT is due to the routing used for the member list view + //but this is now configured to use the key, so this is just a fail safe + if (id && id.length < 9) { + entityResource.getById(id, 'Member').then(function (entity) { + $location.path('/member/member/edit/' + entity.key); + deferred.resolve($scope.content); + }, function () { + deferred.reject(); + }); + } else { + //we are editing so get the content item from the server + memberResource.getByKey(id).then(function (data) { + $scope.content = data; + if (!infiniteMode) { + var path = buildTreePath(data); + navigationService.syncTree({ + tree: 'member', + path: path.split(','), + forceReload: true + }); //syncTreeNode($scope.content, data.path, true); + } + //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.notifyAndClearAllSubscriptions(); + init(); + $scope.page.loading = false; + deferred.resolve($scope.content); + }, function () { + deferred.reject(); + }); + } + return deferred.promise; + } $scope.appChanged = function (app) { $scope.app = app; // setup infinite mode @@ -12572,7 +13570,7 @@ } }; $scope.showBack = function () { - return !!listName; + return !infiniteMode && !!listName; }; /** Callback for when user clicks the back-icon */ $scope.onBack = function () { @@ -12586,6 +13584,12 @@ var memberKey = $scope.content.key; memberResource.exportMemberData(memberKey); }; + //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.Member.EditController', MemberEditController); 'use strict'; @@ -12725,6 +13729,10 @@ }); $scope.page.saveButtonState = 'success'; }, function (err) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); contentEditingHelper.handleSaveError({ err: err }); $scope.page.saveButtonState = 'error'; //share state @@ -12744,6 +13752,64 @@ } angular.module('umbraco').controller('Umbraco.Editors.MemberGroups.EditController', MemberGroupsEditController); 'use strict'; + angular.module('umbraco').controller('Umbraco.Editors.MemberTypes.CopyController', function ($scope, memberTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + $scope.dialogTreeApi = {}; + $scope.source = _.clone($scope.currentNode); + function nodeSelectHandler(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; + memberTypeResource.copy({ + parentId: $scope.target.id, + id: $scope.source.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: 'memberTypes', + 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: 'memberTypes', + path: activeNodePath, + forceReload: false, + activate: true + }); + } + }); + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + }); + }; + $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + }; + $scope.close = function () { + navigationService.hideDialog(); + }; + }); + 'use strict'; /** * @ngdoc controller * @name Umbraco.Editors.MemberTypes.CreateController @@ -12776,8 +13842,16 @@ forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: this.createFolderForm + }); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: this.createFolderForm, + hasErrors: true + }); // TODO: Handle errors }); } ; @@ -12834,12 +13908,13 @@ */ (function () { 'use strict'; - function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q, localizationService, overlayHelper, contentTypeHelper, angularHelper, eventsService) { + function MemberTypesEditController($scope, $routeParams, $q, memberTypeResource, editorState, iconHelper, navigationService, contentEditingHelper, notificationsService, localizationService, overlayHelper, contentTypeHelper, angularHelper, eventsService) { var evts = []; var vm = this; var infiniteMode = $scope.model && $scope.model.infiniteMode; - var memberTypeId = infiniteMode ? $scope.model.id : $routeParams.id; - var create = infiniteMode ? $scope.model.create : $routeParams.create; + var memberTypeId = $routeParams.id; + var create = $routeParams.create; + var memberTypeIcon = ''; vm.save = save; vm.close = close; vm.editorfor = 'visuallyHiddenTexts_newMember'; @@ -12852,11 +13927,22 @@ vm.page.loading = false; vm.page.saveButtonState = 'init'; vm.labels = {}; - vm.saveButtonKey = infiniteMode ? 'buttons_saveAndClose' : 'buttons_save'; - var labelKeys = [ - 'general_design', - 'shortcuts_shortcut', - 'shortcuts_addGroup', + vm.saveButtonKey = 'buttons_save'; + vm.generateModelsKey = 'buttons_saveAndGenerateModels'; + onInit(); + function onInit() { + // get init values from model when in infinite mode + if (infiniteMode) { + memberTypeId = $scope.model.id; + create = $scope.model.create; + vm.saveButtonKey = 'buttons_saveAndClose'; + vm.generateModelsKey = 'buttons_generateModelsAndClose'; + } + } + var labelKeys = [ + 'general_design', + 'shortcuts_shortcut', + 'shortcuts_addGroup', 'shortcuts_addProperty', 'shortcuts_addEditor', 'shortcuts_editDataType' @@ -12921,7 +14007,6 @@ hotKeyWhenHidden: true, labelKey: vm.saveButtonKey, letter: 'S', - type: 'submit', handler: function handler() { vm.save(); } @@ -12976,6 +14061,9 @@ vm.page.loading = false; }); } else { + loadMemberType(); + } + function loadMemberType() { vm.page.loading = true; memberTypeResource.getById(memberTypeId).then(function (dt) { init(dt); @@ -12985,6 +14073,7 @@ vm.page.loading = false; }); } + /* ---------- SAVE ---------- */ function save() { // only save if there is no overlays open if (overlayHelper.getNumberOfOverlays() === 0) { @@ -12994,44 +14083,21 @@ saveMethod: memberTypeResource.save, scope: $scope, content: vm.contentType, - // we need to rebind... the IDs that have been created! - rebindCallback: function rebindCallback(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; - }); - }); + rebindCallback: function rebindCallback(_, savedContentType) { + // we need to rebind... the IDs that have been created! + contentTypeHelper.rebindSavedContentType(vm.contentType, savedContentType); } }).then(function (data) { //success if (!infiniteMode) { syncTreeNode(vm.contentType, data.path); } + // emit event + var args = { memberType: vm.contentType }; + eventsService.emit('editors.memberType.saved', args); + if (memberTypeIcon !== vm.contentType.icon) { + eventsService.emit('editors.tree.icon.changed', args); + } vm.page.saveButtonState = 'success'; if (infiniteMode && $scope.model.submit) { $scope.model.submit(); @@ -13055,20 +14121,12 @@ } } 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; + memberTypeIcon = contentType.icon; } function convertLegacyIcons(contentType) { // make array to store contentType icon @@ -13080,14 +14138,6 @@ // 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({ @@ -13103,6 +14153,9 @@ $scope.model.close(); } } + evts.push(eventsService.on('app.refreshEditor', function (name, error) { + loadMemberType(); + })); evts.push(eventsService.on('editors.groupsBuilder.changed', function (name, args) { angularHelper.getCurrentForm($scope).$setDirty(); })); @@ -13151,6 +14204,21 @@ vm.selectDataType = selectDataType; vm.labels = {}; vm.versionRegex = /^(\d+\.)(\d+\.)(\*|\d+)$/; + vm.aceOption = { + mode: 'xml', + theme: 'chrome', + showPrintMargin: false, + advanced: { + fontSize: '14px', + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: false + }, + onLoad: function onLoad(_editor) { + vm.editor = _editor; + vm.editor.setValue(vm.package.actions); + } + }; function onInit() { if (create) { // Pre populate package with some values @@ -13280,7 +14348,10 @@ packageResource.savePackage(vm.package).then(function (updatedPackage) { vm.package = updatedPackage; vm.buttonState = 'success'; - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: editPackageForm + }); if (create) { //if we are creating, then redirect to the correct url and reload $location.path('packages/packages/edit/' + vm.package.id).search('create', null); @@ -13288,6 +14359,11 @@ $location.replace(); } }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: editPackageForm, + hasErrors: true + }); formHelper.handleError(err); vm.buttonState = 'error'; }); @@ -13312,7 +14388,7 @@ editorService.contentPicker(contentPicker); } function openFilePicker() { - var selection = angular.copy(vm.package.files); + var selection = Utilities.copy(vm.package.files); var filePicker = { title: 'Select files', section: 'settings', @@ -13601,7 +14677,7 @@ vm.createdPackages = []; packageResource.getAllCreated().then(function (createdPackages) { vm.createdPackages = createdPackages; - }, angular.noop); + }, Utilities.noop); } function deleteCreatedPackage(event, index, createdPackage) { event.stopPropagation(); @@ -13864,7 +14940,7 @@ 'use strict'; (function () { 'use strict'; - function PackagesRepoController($scope, $route, $location, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { + function PackagesRepoController($scope, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { var vm = this; vm.packageViewState = 'packageList'; vm.categories = []; @@ -13926,7 +15002,9 @@ }); $q.all([ ourPackageRepositoryResource.getCategories().then(function (cats) { - vm.categories = cats; + vm.categories = cats.filter(function (cat) { + return cat.name !== 'Umbraco Pro'; + }); }), ourPackageRepositoryResource.getPopular(8).then(function (pack) { vm.popular = pack.packages; @@ -14157,9 +15235,17 @@ forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: form + }); var section = appState.getSectionState('currentSection'); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: form, + hasErrors: true + }); vm.createFolderError = err; }); } @@ -14288,7 +15374,7 @@ activate: false }); completeSave(saved); - }, angular.noop); + }, Utilities.noop); } else { completeSave(saved); } @@ -14565,9 +15651,17 @@ forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: form + }); var section = appState.getSectionState('currentSection'); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: form, + hasErrors: true + }); vm.createFolderError = err; }); } @@ -15010,18 +16104,77 @@ }; }); 'use strict'; + angular.module('umbraco').controller('Umbraco.PrevalueEditors.CheckboxListController', function ($scope) { + var vm = this; + vm.configItems = []; + vm.viewItems = []; + vm.change = change; + function init() { + var prevalues = ($scope.model.config ? $scope.model.config.prevalues : $scope.model.prevalues) || []; + var items = []; + for (var i = 0; i < prevalues.length; i++) { + var item = {}; + if (Utilities.isObject(prevalues[i])) { + item.value = prevalues[i].value; + item.label = prevalues[i].label; + } else { + item.value = prevalues[i]; + item.label = prevalues[i]; + } + items.push({ + value: item.value, + label: item.label + }); + } + vm.configItems = items; + if ($scope.model.value === null || $scope.model.value === undefined) { + $scope.model.value = []; + } + // update view model. + generateViewModel($scope.model.value); + } + function generateViewModel(newVal) { + vm.viewItems = []; + var iConfigItem; + for (var i = 0; i < vm.configItems.length; i++) { + iConfigItem = vm.configItems[i]; + var isChecked = _.contains(newVal, iConfigItem.value); + vm.viewItems.push({ + checked: isChecked, + value: iConfigItem.value, + label: iConfigItem.label + }); + } + } + function change(model, value) { + var index = $scope.model.value.indexOf(value); + if (model === true) { + //if it doesn't exist in the model, then add it + if (index < 0) { + $scope.model.value.push(value); + } + } else { + //if it exists in the model, then remove it + if (index >= 0) { + $scope.model.value.splice(index, 1); + } + } + } + init(); + }); + 'use strict'; angular.module('umbraco').controller('Umbraco.PrevalueEditors.ColorPickerController', function ($scope) { //setup the default config var config = { useLabel: false }; //map the user config - angular.extend(config, $scope.model.config); + Utilities.extend(config, $scope.model.config); //map back to the model $scope.model.config = config; $scope.isConfigured = $scope.model.prevalues && _.keys($scope.model.prevalues).length > 0; $scope.model.items = []; // Make an array from the dictionary var items = []; - if (angular.isArray($scope.model.prevalues)) { + if (Utilities.isArray($scope.model.prevalues)) { for (var i in $scope.model.prevalues) { var oldValue = $scope.model.prevalues[i]; if (!isValidHex(oldValue.value || oldValue)) @@ -15138,7 +16291,7 @@ }; //combine the dialogOptions with any values returned from the server if ($scope.model.config) { - angular.extend(dialogOptions, $scope.model.config); + Utilities.extend(dialogOptions, $scope.model.config); } $scope.openTreePicker = function () { var treePicker = dialogOptions; @@ -15224,7 +16377,7 @@ $scope.newItem = ''; $scope.hasError = false; $scope.focusOnNew = false; - if (!angular.isArray($scope.model.value)) { + if (!Utilities.isArray($scope.model.value)) { //make an array from the dictionary var items = []; for (var i in $scope.model.value) { @@ -15296,6 +16449,12 @@ } }); 'use strict'; + angular.module('umbraco').controller('Umbraco.PrevalueEditors.OverlaySizeController', function ($scope) { + if (!$scope.model.value) { + $scope.model.value = 'small'; + } + }); + 'use strict'; //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, entityResource, iconHelper, editorService) { @@ -15313,10 +16472,10 @@ }; //combine the config with any values returned from the server if ($scope.model.config) { - angular.extend(config, $scope.model.config); + Utilities.extend(config, $scope.model.config); } if ($scope.model.value) { - if (Array.isArray($scope.model.value)) { + if (!Array.isArray($scope.model.value)) { $scope.ids = $scope.model.value.split(','); } else { $scope.ids.push($scope.model.value); @@ -15339,7 +16498,7 @@ }); }); } - $scope.openContentPicker = function () { + $scope.openTreePicker = function () { var treePicker = config; treePicker.section = config.type; treePicker.submit = function (model) { @@ -15397,7 +16556,7 @@ return str.replace(rgxtrim, ''); } function populate(data) { - if (angular.isArray(data)) { + if (Utilities.isArray(data)) { _.each(data, function (item, i) { $scope.add(item); }); @@ -15564,7 +16723,8 @@ $scope.model.value = _.pluck(vm.itemTypes, 'alias').join(); angularHelper.getCurrentForm($scope).$setDirty(); } - eventsService.on('treeSourceChanged', function (e, args) { + var evts = []; + evts.push(eventsService.on('treeSourceChanged', function (e, args) { // reset the model value if we changed node type (but not on the initial load) if (!!currentItemType && currentItemType !== args.value) { vm.itemTypes = []; @@ -15572,133 +16732,712 @@ } currentItemType = args.value; init(); + })); + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } }); + if ($scope.model.config.itemType) { + currentItemType = $scope.model.config.itemType; + init(); + } } angular.module('umbraco').controller('Umbraco.PrevalueEditors.TreeSourceTypePickerController', TreeSourceTypePickerController); 'use strict'; - function booleanEditorController($scope, angularHelper) { - function setupViewModel() { - $scope.renderModel = { value: false }; - if ($scope.model.config && $scope.model.config.default && Object.toBoolean($scope.model.config.default) && $scope.model && !$scope.model.value) { - $scope.renderModel.value = true; - } - if ($scope.model && $scope.model.value && Object.toBoolean($scope.model.value)) { - $scope.renderModel.value = true; - } - } - setupViewModel(); - if ($scope.model && !$scope.model.value) { - $scope.model.value = $scope.renderModel.value === true ? '1' : '0'; + (function () { + 'use strict'; + function InlineBlockEditor($scope) { + var bc = this; + bc.openBlock = function (block) { + // if we are closing: + if (block.active === true) { + // boardcast the formSubmitting event to trigger syncronization or none-live property-editors + $scope.$broadcast('formSubmitting', { scope: $scope }); + // Some property editors need to performe an action after all property editors have reacted to the formSubmitting. + $scope.$broadcast('formSubmittingFinalPhase', { scope: $scope }); + block.active = false; + } else { + $scope.api.activateBlock(block); + } + }; } - //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(); - }; - // Update the value when the toggle is clicked - $scope.toggle = function () { - angularHelper.getCurrentForm($scope).$setDirty(); - if ($scope.renderModel.value) { - $scope.model.value = '0'; - setupViewModel(); - return; - } - $scope.model.value = '1'; - setupViewModel(); - }; - } - angular.module('umbraco').controller('Umbraco.PropertyEditors.BooleanController', booleanEditorController); + angular.module('umbraco').controller('Umbraco.PropertyEditors.BlockEditor.InlineBlockEditor', InlineBlockEditor); + }()); 'use strict'; - 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 + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + return obj; } - */ - //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; + /** + * @ngdoc controller + * @name Umbraco.Editors.BlockList.BlockConfigurationController + * @function + * + * @description + * The controller for the content type editor property settings dialog + */ + (function () { + 'use strict'; + function TransferProperties(fromObject, toObject) { + for (var p in fromObject) { + toObject[p] = fromObject[p]; } - if (!$scope.model.config || $scope.model.config.enableReset === undefined) { - $scope.model.config.enableReset = true; + } + function BlockConfigurationController($scope, elementTypeResource, overlayService, localizationService, editorService, eventsService, udiService) { + var unsubscribe = []; + var vm = this; + vm.openBlock = null; + function onInit() { + if (!$scope.model.value) { + $scope.model.value = []; + } + loadElementTypes(); } - if (!$scope.model.config || $scope.model.config.minPasswordLength === undefined) { - $scope.model.config.minPasswordLength = 0; + function loadElementTypes() { + return elementTypeResource.getAll().then(function (elementTypes) { + vm.elementTypes = elementTypes; + }); } - //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; + function updateUsedElementTypes(event, args) { + var key = args.documentType.key; + for (var i = 0; i < vm.elementTypes.length; i++) { + if (vm.elementTypes[i].key === key) { + vm.elementTypes[i] = args.documentType; + } } - $scope.model.value.reset = null; - $scope.model.value.answer = null; } - } - resetModel(); - }); - 'use strict'; - angular.module('umbraco').controller('Umbraco.PropertyEditors.CheckboxListController', function ($scope) { - var vm = this; - vm.configItems = []; - vm.viewItems = []; - vm.change = change; - function init() { - // currently the property editor will onyl work if our input is an object. - if (angular.isObject($scope.model.config.items)) { - // formatting the items in the dictionary into an array - var sortedItems = []; - var vals = _.values($scope.model.config.items); - var keys = _.keys($scope.model.config.items); - for (var i = 0; i < vals.length; i++) { - sortedItems.push({ - key: keys[i], - sortOrder: vals[i].sortOrder, - value: vals[i].value + unsubscribe.push(eventsService.on('editors.documentType.saved', updateUsedElementTypes)); + vm.requestRemoveBlockByIndex = function (index) { + localizationService.localizeMany([ + 'general_delete', + 'blockEditor_confirmDeleteBlockTypeMessage', + 'blockEditor_confirmDeleteBlockTypeNotice' + ]).then(function (data) { + var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey); + overlayService.confirmDelete({ + title: data[0], + content: localizationService.tokenReplace(data[1], [contentElementType ? contentElementType.name : '(Unavailable ElementType)']), + confirmMessage: data[2], + close: function close() { + overlayService.close(); + }, + submit: function submit() { + vm.removeBlockByIndex(index); + overlayService.close(); + } }); - } - // ensure the items are sorted by the provided sort order - sortedItems.sort(function (a, b) { - return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; }); - vm.configItems = sortedItems; - if ($scope.model.value === null || $scope.model.value === undefined) { - $scope.model.value = []; + }; + vm.removeBlockByIndex = function (index) { + $scope.model.value.splice(index, 1); + }; + vm.sortableOptions = { + 'ui-floating': true, + items: 'umb-block-card', + cursor: 'grabbing', + placeholder: 'umb-block-card --sortable-placeholder' + }; + vm.getAvailableElementTypes = function () { + return vm.elementTypes.filter(function (type) { + return !$scope.model.value.find(function (entry) { + return type.key === entry.contentElementTypeKey; + }); + }); + }; + vm.getElementTypeByKey = function (key) { + if (vm.elementTypes) { + return vm.elementTypes.find(function (type) { + return type.key === key; + }) || null; } - // update view model. + }; + vm.openAddDialog = function () { + localizationService.localize('blockEditor_headlineCreateBlock').then(function (localizedTitle) { + var contentTypePicker = { + title: localizedTitle, + section: 'settings', + treeAlias: 'documentTypes', + entityType: 'documentType', + isDialog: true, + filter: function filter(node) { + if (node.metaData.isElement === true) { + var key = udiService.getKey(node.udi); + // If a Block with this ElementType as content already exists, we will emit it as a posible option. + return $scope.model.value.find(function (entry) { + return key === entry.contentElementTypeKey; + }); + } + return true; + }, + filterCssClass: 'not-allowed', + select: function select(node) { + vm.addBlockFromElementTypeKey(udiService.getKey(node.udi)); + editorService.close(); + }, + close: function close() { + editorService.close(); + }, + extraActions: [{ + style: 'primary', + labelKey: 'blockEditor_labelcreateNewElementType', + action: function action() { + vm.createElementTypeAndCallback(function (documentTypeKey) { + vm.addBlockFromElementTypeKey(documentTypeKey); + // At this point we will close the contentTypePicker. + editorService.close(); + }); + } + }] + }; + editorService.treePicker(contentTypePicker); + }); + }; + vm.createElementTypeAndCallback = function (callback) { + var _editor; + var editor = (_editor = { + create: true, + infiniteMode: true, + noTemplate: true, + isElement: true + }, _defineProperty(_editor, 'noTemplate', true), _defineProperty(_editor, 'submit', function submit(model) { + loadElementTypes().then(function () { + callback(model.documentTypeKey); + }); + editorService.close(); + }), _defineProperty(_editor, 'close', function close() { + editorService.close(); + }), _editor); + editorService.documentTypeEditor(editor); + }; + vm.addBlockFromElementTypeKey = function (key) { + var blockType = { + 'contentElementTypeKey': key, + 'settingsElementTypeKey': null, + 'labelTemplate': '', + 'view': null, + 'stylesheet': null, + 'editorSize': 'medium', + 'iconColor': null, + 'backgroundColor': null, + 'thumbnail': null + }; + $scope.model.value.push(blockType); + vm.openBlockOverlay(blockType); + }; + vm.openBlockOverlay = function (block) { + var elementType = vm.getElementTypeByKey(block.contentElementTypeKey); + if (elementType) { + localizationService.localize('blockEditor_blockConfigurationOverlayTitle', [elementType.name]).then(function (data) { + var clonedBlockData = Utilities.copy(block); + vm.openBlock = block; + var overlayModel = { + block: clonedBlockData, + title: data, + view: 'views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html', + size: 'small', + submit: function submit(overlayModel) { + loadElementTypes(); + // lets load elementType again, to ensure we are up to date. + TransferProperties(overlayModel.block, block); + // transfer properties back to block object. (Doing this cause we dont know if block object is added to model jet, therefor we cant use index or replace the object.) + overlayModel.close(); + }, + close: function close() { + editorService.close(); + vm.openBlock = null; + } + }; + // open property settings editor + editorService.open(overlayModel); + }); + } else { + alert('Cannot be edited cause ElementType does not exist.'); + } + }; + $scope.$on('$destroy', function () { + unsubscribe.forEach(function (u) { + u(); + }); + }); + onInit(); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.BlockList.BlockConfigurationController', BlockConfigurationController); + }()); + 'use strict'; + /** + * @ngdoc controller + * @name Umbraco.Editors.BlockList.BlockConfigurationOverlayController + * @function + * + * @description + * The controller for the content type editor property settings dialog + */ + (function () { + 'use strict'; + function BlockConfigurationOverlayController($scope, overlayService, localizationService, editorService, elementTypeResource, eventsService, udiService, angularHelper) { + var unsubscribe = []; + var vm = this; + vm.block = $scope.model.block; + vm.colorPickerOptions = { + type: 'color', + allowEmpty: true, + showAlpha: true + }; + loadElementTypes(); + function loadElementTypes() { + return elementTypeResource.getAll().then(function (elementTypes) { + vm.elementTypes = elementTypes; + vm.contentPreview = vm.getElementTypeByKey(vm.block.contentElementTypeKey); + vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey); + }); + } + vm.getElementTypeByKey = function (key) { + return vm.elementTypes.find(function (type) { + return type.key === key; + }); + }; + vm.openElementType = function (elementTypeKey) { + var elementType = vm.getElementTypeByKey(elementTypeKey); + if (elementType) { + var elementTypeId = elementType.id; + var editor = { + id: elementTypeId, + submit: function submit(model) { + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + } + }; + vm.createElementTypeAndCallback = function (callback) { + var editor = { + create: true, + infiniteMode: true, + isElement: true, + noTemplate: true, + submit: function submit(model) { + callback(model.documentTypeKey); + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + }; + vm.addSettingsForBlock = function ($event, block) { + localizationService.localize('blockEditor_headlineAddSettingsElementType').then(function (localizedTitle) { + var settingsTypePicker = { + title: localizedTitle, + section: 'settings', + treeAlias: 'documentTypes', + entityType: 'documentType', + isDialog: true, + filter: function filter(node) { + if (node.metaData.isElement === true) { + return false; + } + return true; + }, + filterCssClass: 'not-allowed', + select: function select(node) { + vm.applySettingsToBlock(block, udiService.getKey(node.udi)); + editorService.close(); + }, + close: function close() { + editorService.close(); + }, + extraActions: [{ + style: 'primary', + labelKey: 'blockEditor_labelcreateNewElementType', + action: function action() { + vm.createElementTypeAndCallback(function (key) { + vm.applySettingsToBlock(block, key); + // At this point we will close the contentTypePicker. + editorService.close(); + }); + } + }] + }; + editorService.treePicker(settingsTypePicker); + }); + }; + vm.applySettingsToBlock = function (block, key) { + block.settingsElementTypeKey = key; + vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey); + }; + vm.requestRemoveSettingsForBlock = function (block) { + localizationService.localizeMany([ + 'general_remove', + 'defaultdialogs_confirmremoveusageof' + ]).then(function (data) { + var settingsElementType = vm.getElementTypeByKey(block.settingsElementTypeKey); + overlayService.confirmRemove({ + title: data[0], + content: localizationService.tokenReplace(data[1], [settingsElementType ? settingsElementType.name : '(Unavailable ElementType)']), + close: function close() { + overlayService.close(); + }, + submit: function submit() { + vm.removeSettingsForBlock(block); + overlayService.close(); + } + }); + }); + }; + vm.removeSettingsForBlock = function (block) { + block.settingsElementTypeKey = null; + }; + function updateUsedElementTypes(event, args) { + var key = args.documentType.key; + for (var i = 0; i < vm.elementTypes.length; i++) { + if (vm.elementTypes[i].key === key) { + vm.elementTypes[i] = args.documentType; + } + } + if (vm.contentPreview && vm.contentPreview.key === key) { + vm.contentPreview = args.documentType; + $scope.$evalAsync(); + } + if (vm.settingsPreview && vm.settingsPreview.key === key) { + vm.settingsPreview = args.documentType; + $scope.$evalAsync(); + } + } + unsubscribe.push(eventsService.on('editors.documentType.saved', updateUsedElementTypes)); + vm.addViewForBlock = function (block) { + localizationService.localize('blockEditor_headlineAddCustomView').then(function (localizedTitle) { + var filePicker = { + title: localizedTitle, + section: 'settings', + treeAlias: 'files', + entityType: 'file', + isDialog: true, + filter: function filter(i) { + return !(i.name.indexOf('.html') !== -1); + }, + filterCssClass: 'not-allowed', + select: function select(node) { + var filepath = decodeURIComponent(node.id.replace(/\+/g, ' ')); + block.view = '~/' + filepath; + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.treePicker(filePicker); + }); + }; + vm.requestRemoveViewForBlock = function (block) { + localizationService.localizeMany([ + 'general_remove', + 'defaultdialogs_confirmremoveusageof' + ]).then(function (data) { + overlayService.confirmRemove({ + title: data[0], + content: localizationService.tokenReplace(data[1], [block.view]), + close: function close() { + overlayService.close(); + }, + submit: function submit() { + vm.removeViewForBlock(block); + overlayService.close(); + } + }); + }); + }; + vm.removeViewForBlock = function (block) { + block.view = null; + }; + vm.addStylesheetForBlock = function (block) { + localizationService.localize('blockEditor_headlineAddCustomStylesheet').then(function (localizedTitle) { + var filePicker = { + title: localizedTitle, + section: 'settings', + treeAlias: 'files', + entityType: 'file', + isDialog: true, + filter: function filter(i) { + return !(i.name.indexOf('.css') !== -1); + }, + filterCssClass: 'not-allowed', + select: function select(node) { + var filepath = decodeURIComponent(node.id.replace(/\+/g, ' ')); + block.stylesheet = '~/' + filepath; + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.treePicker(filePicker); + }); + }; + vm.requestRemoveStylesheetForBlock = function (block) { + localizationService.localizeMany([ + 'general_remove', + 'defaultdialogs_confirmremoveusageof' + ]).then(function (data) { + overlayService.confirmRemove({ + title: data[0], + content: localizationService.tokenReplace(data[1], [block.stylesheet]), + close: function close() { + overlayService.close(); + }, + submit: function submit() { + vm.removeStylesheetForBlock(block); + overlayService.close(); + } + }); + }); + }; + vm.removeStylesheetForBlock = function (block) { + block.stylesheet = null; + }; + vm.addThumbnailForBlock = function (block) { + localizationService.localize('blockEditor_headlineAddThumbnail').then(function (localizedTitle) { + var thumbnailPicker = { + title: localizedTitle, + section: 'settings', + treeAlias: 'files', + entityType: 'file', + isDialog: true, + filter: function filter(i) { + return !(i.name.indexOf('.jpg') !== -1 || i.name.indexOf('.jpeg') !== -1 || i.name.indexOf('.png') !== -1 || i.name.indexOf('.svg') !== -1 || i.name.indexOf('.webp') !== -1 || i.name.indexOf('.gif') !== -1); + }, + filterCssClass: 'not-allowed', + select: function select(file) { + var id = decodeURIComponent(file.id.replace(/\+/g, ' ')); + block.thumbnail = '~/' + id; + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.treePicker(thumbnailPicker); + }); + }; + vm.removeThumbnailForBlock = function (entry) { + entry.thumbnail = null; + }; + vm.changeIconColor = function (color) { + angularHelper.safeApply($scope, function () { + vm.block.iconColor = color ? color.toString() : null; + }); + }; + vm.changeBackgroundColor = function (color) { + angularHelper.safeApply($scope, function () { + vm.block.backgroundColor = color ? color.toString() : null; + }); + }; + vm.submit = function () { + if ($scope.model && $scope.model.submit) { + $scope.model.submit($scope.model); + } + }; + vm.close = function () { + if ($scope.model && $scope.model.close) { + $scope.model.close($scope.model); + } + }; + $scope.$on('$destroy', function () { + unsubscribe.forEach(function (u) { + u(); + }); + }); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.BlockList.BlockConfigurationOverlayController', BlockConfigurationOverlayController); + }()); + 'use strict'; + (function () { + 'use strict'; + angular.module('umbraco').controller('Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController', function Controller($scope) { + var vm = this; + vm.plusPosX = 0; + vm.onMouseMove = function ($event) { + vm.plusPosX = $event.offsetX; + }; + }); + }()); + 'use strict'; + function booleanEditorController($scope) { + // Setup the default config + // This allow to overwrite the configuration when property editor is re-used + // in e.g. third party packages, dashboard or content app. For example when using umb-property-editor. + // At the moment this use "1/0" as default for "truevalue" and "falsevalue", but allow "True/False" as well. + // Maybe sometime later we can make it support "Yes/No" or "On/Off" as well similar to ng-true-value and ng-false-value in Angular. + var config = { + truevalue: '1', + falsevalue: '0', + showLabels: false + }; + if ($scope.model.config) { + $scope.model.config.showLabels = $scope.model.config.showLabels ? Object.toBoolean($scope.model.config.showLabels) : config.showLabels; + } + // Map the user config + Utilities.extend(config, $scope.model.config); + // Map back to the model + $scope.model.config = config; + function setupViewModel() { + $scope.renderModel = { value: false }; + if ($scope.model.config && $scope.model.config.default && Object.toBoolean($scope.model.config.default) && $scope.model && !$scope.model.value) { + $scope.renderModel.value = true; + } + if ($scope.model && $scope.model.value && Object.toBoolean($scope.model.value)) { + $scope.renderModel.value = true; + } + } + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + setupViewModel(); + if ($scope.model && !$scope.model.value) { + $scope.model.value = $scope.renderModel.value === true ? $scope.model.config.truevalue : $scope.model.config.falsevalue; + } + // 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(); + }; + // If another property editor changes the model.value we want to pick that up and reflect the value in this one. + var unsubscribe = $scope.$watch('model.value', function (newVal, oldVal) { + if (newVal !== oldVal) { + setupViewModel(); + } + }); + // Update the value when the toggle is clicked + $scope.toggle = function () { + setDirty(); + if ($scope.renderModel.value) { + $scope.model.value = $scope.model.config.falsevalue; + setupViewModel(); + return; + } + $scope.model.value = $scope.model.config.truevalue; + setupViewModel(); + }; + $scope.$on('$destroy', function () { + unsubscribe(); + }); + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.BooleanController', booleanEditorController); + 'use strict'; + 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; + } + if (!$scope.model.config || $scope.model.config.minNonAlphaNumericChars === undefined) { + $scope.model.config.minNonAlphaNumericChars = 0; + } + //set the model defaults + if (!Utilities.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(); + }); + 'use strict'; + angular.module('umbraco').controller('Umbraco.PropertyEditors.CheckboxListController', function ($scope, validationMessageService) { + var vm = this; + vm.configItems = []; + vm.viewItems = []; + vm.change = change; + function init() { + vm.uniqueId = String.CreateGuid(); + // currently the property editor will only work if our input is an object. + if (Utilities.isObject($scope.model.config.items)) { + // formatting the items in the dictionary into an array + var sortedItems = []; + var vals = _.values($scope.model.config.items); + var keys = _.keys($scope.model.config.items); + for (var i = 0; i < vals.length; i++) { + sortedItems.push({ + key: keys[i], + sortOrder: vals[i].sortOrder, + value: vals[i].value + }); + } + // ensure the items are sorted by the provided sort order + sortedItems.sort(function (a, b) { + return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; + }); + vm.configItems = sortedItems; + if ($scope.model.value === null || $scope.model.value === undefined) { + $scope.model.value = []; + } + // update view model. generateViewModel($scope.model.value); //watch the model.value in case it changes so that we can keep our view model in sync $scope.$watchCollection('model.value', updateViewModel); } + // Set the message to use for when a mandatory field isn't completed. + // Will either use the one provided on the property type or a localised default. + validationMessageService.getMandatoryMessage($scope.model.validation).then(function (value) { + $scope.mandatoryMessage = value; + }); } function updateViewModel(newVal) { var i = vm.configItems.length; @@ -15743,14 +17482,15 @@ }); 'use strict'; function ColorPickerController($scope, $timeout) { - //setup the default config + var vm = this; + // setup the default config var config = { items: [], multiple: false }; - //map the user config - angular.extend(config, $scope.model.config); - //map back to the model + // map the user config + Utilities.extend(config, $scope.model.config); + // map back to the model $scope.model.config = config; $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; if ($scope.isConfigured) { @@ -15761,10 +17501,10 @@ label: $scope.model.config.items[key] }; } - $scope.model.useLabel = isTrue($scope.model.config.useLabel); + $scope.model.useLabel = Object.toBoolean($scope.model.config.useLabel); initActiveColor(); } - if (!angular.isArray($scope.model.config.items)) { + if (!Utilities.isArray($scope.model.config.items)) { //make an array from the dictionary var items = []; for (var i in $scope.model.config.items) { @@ -15792,11 +17532,11 @@ //now make the editor model the array $scope.model.config.items = items; } - $scope.selectColor = function (color) { + vm.selectColor = function (color) { // this is required to re-validate $timeout(function () { var newColor = color ? color.value : null; - $scope.propertyForm.selectedColor.$setViewValue(newColor); + vm.modelValueForm.selectedColor.$setViewValue(newColor); }); }; // Method required by the valPropertyValidator directive (returns true if the property editor has at least one color selected) @@ -15808,7 +17548,6 @@ errorKey: 'required' }; }; - $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; // 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. @@ -15864,14 +17603,20 @@ $scope.model.value.label = foundItem.label; } } - // figures out if a value is trueish enough - function isTrue(bool) { - return !!bool && bool !== '0' && bool.toString().toLowerCase() !== 'false'; - } } angular.module('umbraco').controller('Umbraco.PropertyEditors.ColorPickerController', ColorPickerController); 'use strict'; - angular.module('umbraco').controller('Umbraco.PrevalueEditors.MultiColorPickerController', function ($scope, $timeout, assetsService, angularHelper, $element, localizationService, eventsService) { + angular.module('umbraco').controller('Umbraco.PrevalueEditors.MultiColorPickerController', function ($scope, angularHelper, $element, eventsService) { + var vm = this; + vm.add = add; + vm.remove = remove; + vm.edit = edit; + vm.cancel = cancel; + vm.show = show; + vm.hide = hide; + vm.change = change; + vm.labelEnabled = false; + vm.editItem = null; //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; @@ -15879,46 +17624,37 @@ $scope.newLabel = defaultLabel; $scope.hasError = false; $scope.focusOnNew = false; - $scope.labels = {}; - var labelKeys = [ - 'general_cancel', - 'general_choose' - ]; - $scope.labelEnabled = false; - eventsService.on('toggleValue', function (e, args) { - $scope.labelEnabled = args.value; - }); - localizationService.localizeMany(labelKeys).then(function (values) { - $scope.labels.cancel = values[0]; - $scope.labels.choose = values[1]; - }); - 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: $scope.labels.choose, - cancelText: $scope.labels.cancel, - preferredFormat: 'hex', - showInput: true, - clickoutFiresChange: true, - hide: function hide(color) { - //show the add butotn - $element.find('.btn.add').show(); - }, - change: function change(color) { - angularHelper.safeApply($scope, function () { - $scope.newColor = color.toHexString().trimStart('#'); // #ff0000 - }); - }, - show: function show() { - //hide the add butotn - $element.find('.btn.add').hide(); + $scope.options = { + type: 'color', + color: defaultColor, + allowEmpty: false, + showAlpha: false + }; + function hide(color) { + // show the add button + $element.find('.btn.add').show(); + } + function show(color) { + // hide the add button + $element.find('.btn.add').hide(); + } + function change(color) { + angularHelper.safeApply($scope, function () { + if (color) { + $scope.newColor = color.toHexString().trimStart('#'); } }); + } + var evts = []; + evts.push(eventsService.on('toggleValue', function (e, args) { + vm.labelEnabled = args.value; + })); + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } }); - if (!angular.isArray($scope.model.value)) { + if (!Utilities.isArray($scope.model.value)) { //make an array from the dictionary var items = []; for (var i in $scope.model.value) { @@ -15954,35 +17690,58 @@ function validLabel(label) { return label !== null && typeof label !== 'undefined' && label !== '' && label.length && label.length > 0; } - $scope.remove = function (item, evt) { + function remove(item, evt) { evt.preventDefault(); $scope.model.value = _.reject($scope.model.value, function (x) { return x.value === item.value && x.label === item.label; }); - angularHelper.getCurrentForm($scope).$setDirty(); - }; - $scope.add = function (evt) { + setDirty(); + } + function add(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(); + return item != vm.editItem && (item.value.toUpperCase() === $scope.newColor.toUpperCase() || item.label.toUpperCase() === newLabel.toUpperCase()); }); if (!exists) { - $scope.model.value.push({ - value: $scope.newColor, - label: newLabel - }); + if (vm.editItem == null) { + $scope.model.value.push({ + value: $scope.newColor, + label: newLabel + }); + } else { + vm.editItem.value = $scope.newColor; + vm.editItem.label = newLabel; + vm.editItem = null; + } $scope.newLabel = ''; $scope.hasError = false; $scope.focusOnNew = true; - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); return; } - //there was an error, do the highlight (will be set back by the directive) + // there was an error, do the highlight (will be set back by the directive) $scope.hasError = true; } - }; + } + function edit(item, evt) { + evt.preventDefault(); + vm.editItem = item; + $scope.newColor = item.value; + $scope.newLabel = item.label; + } + function cancel(evt) { + evt.preventDefault(); + vm.editItem = null; + $scope.newColor = defaultColor; + $scope.newLabel = defaultLabel; + } + function setDirty() { + if (vm.modelValueForm) { + vm.modelValueForm.selectedColor.$setDirty(); + } + } $scope.sortableOptions = { axis: 'y', containment: 'parent', @@ -15991,27 +17750,28 @@ items: '> div.control-group', tolerance: 'pointer', update: function update(e, ui) { - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } }; - //load the separate css for the editor to avoid it blocking our js loading - assetsService.loadCss('lib/spectrum/spectrum.css', $scope); }); 'use strict'; /** * The controller that is used for a couple different Property Editors: Multi Node Tree Picker, Content Picker, * since this is used by MNTP and it supports content, media and members, there is code to deal with all 3 of those types * @param {any} $scope + * @param {any} $q + * @param {any} $routeParams + * @param {any} $location * @param {any} entityResource * @param {any} editorState * @param {any} iconHelper - * @param {any} $routeParams * @param {any} angularHelper * @param {any} navigationService - * @param {any} $location * @param {any} localizationService + * @param {any} editorService + * @param {any} userService */ - function contentPickerController($scope, entityResource, editorState, iconHelper, $routeParams, angularHelper, navigationService, $location, localizationService, editorService, $q) { + function contentPickerController($scope, $q, $routeParams, $location, entityResource, editorState, iconHelper, navigationService, localizationService, editorService, userService, overlayService) { var vm = { labels: { general_recycleBin: '', @@ -16092,21 +17852,32 @@ scroll: true, zIndex: 6000, update: function update(e, ui) { - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } }; + var removeAllEntriesAction = { + labelKey: 'clipboard_labelForRemoveAllEntries', + labelTokens: [], + icon: 'trash', + method: removeAllEntries, + isDisabled: true + }; if ($scope.model.config) { //special case, if the `startNode` is falsy on the server config delete it entirely so the default value is merged in if (!$scope.model.config.startNode) { delete $scope.model.config.startNode; } //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); + $scope.model.config = Utilities.extend(defaultConfig, $scope.model.config); // if the property is mandatory, set the minCount config to 1 (unless of course it is set to something already), // that way the minCount/maxCount validation handles the mandatory as well if ($scope.model.validation && $scope.model.validation.mandatory && !$scope.model.config.minNumber) { $scope.model.config.minNumber = 1; } + if ($scope.model.config.multiPicker === true && $scope.umbProperty) { + var propertyActions = [removeAllEntriesAction]; + $scope.umbProperty.setPropertyActions(propertyActions); + } } //Umbraco persists boolean for prevalues as "0" or "1" so we need to convert that! $scope.model.config.multiPicker = Object.toBoolean($scope.model.config.multiPicker); @@ -16114,7 +17885,7 @@ $scope.model.config.showEditButton = Object.toBoolean($scope.model.config.showEditButton); $scope.model.config.showPathOnHover = Object.toBoolean($scope.model.config.showPathOnHover); var entityType = $scope.model.config.startNode.type === 'member' ? 'Member' : $scope.model.config.startNode.type === 'media' ? 'Media' : 'Document'; - $scope.allowOpenButton = entityType === 'Document'; + $scope.allowOpenButton = false; $scope.allowEditButton = entityType === 'Document'; $scope.allowRemoveButton = true; //the dialog options for the picker @@ -16126,7 +17897,7 @@ dataTypeKey: $scope.model.dataTypeKey, currentNode: editorState ? editorState.current : null, callback: function callback(data) { - if (angular.isArray(data)) { + if (Utilities.isArray(data)) { _.each(data, function (item, i) { $scope.add(item); }); @@ -16134,7 +17905,7 @@ $scope.clear(); $scope.add(data); } - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); }, treeAlias: $scope.model.config.startNode.type, section: $scope.model.config.startNode.type, @@ -16142,7 +17913,7 @@ }; //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); + Utilities.extend(dialogOptions, $scope.model.config); dialogOptions.dataTypeKey = $scope.model.dataTypeKey; // if we can't pick more than one item, explicitly disable multiPicker in the dialog options if ($scope.model.config.maxNumber && parseInt($scope.model.config.maxNumber) === 1) { @@ -16197,13 +17968,13 @@ $scope.openCurrentPicker = function () { $scope.currentPicker = dialogOptions; $scope.currentPicker.submit = function (model) { - if (angular.isArray(model.selection)) { + if (Utilities.isArray(model.selection)) { _.each(model.selection, function (item, i) { $scope.add(item); }); - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); editorService.close(); }; $scope.currentPicker.close = function () { @@ -16227,9 +17998,10 @@ var currIds = $scope.model.value ? $scope.model.value.split(',') : []; if (currIds.length > 0) { currIds.splice(index, 1); - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); $scope.model.value = currIds.join(); } + removeAllEntriesAction.isDisabled = currIds.length === 0; }; $scope.showNode = function (index) { var item = $scope.renderModel[index]; @@ -16254,20 +18026,25 @@ currIds.push(itemId); $scope.model.value = currIds.join(); } + removeAllEntriesAction.isDisabled = false; }; $scope.clear = function () { $scope.model.value = null; + removeAllEntriesAction.isDisabled = true; }; - $scope.openContentEditor = function (node) { - var contentEditor = { - id: node.id, + $scope.openEditor = function (item) { + var editor = { + id: entityType === 'Member' ? item.key : item.id, submit: function submit(model) { + var node = entityType === 'Member' ? model.memberNode : entityType === 'Media' ? model.mediaNode : model.contentNode; // update the node - node.name = model.contentNode.name; - node.published = model.contentNode.hasPublishedVersion; + item.name = node.name; if (entityType !== 'Member') { - entityResource.getUrl(model.contentNode.id, entityType).then(function (data) { - node.url = data; + if (entityType === 'Document') { + item.published = node.hasPublishedVersion; + } + entityResource.getUrl(node.id, entityType).then(function (data) { + item.url = data; }); } editorService.close(); @@ -16276,7 +18053,17 @@ editorService.close(); } }; - editorService.contentEditor(contentEditor); + switch (entityType) { + case 'Document': + editorService.contentEditor(editor); + break; + case 'Media': + editorService.mediaEditor(editor); + break; + case 'Member': + editorService.memberEditor(editor); + break; + } }; //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { @@ -16284,11 +18071,17 @@ unsubscribe(); } }); + function setDirty() { + if ($scope.contentPickerForm && $scope.contentPickerForm.modelValue) { + $scope.contentPickerForm.modelValue.$setDirty(); + } + } /** Syncs the renderModel based on the actual model.value and returns a promise */ function syncRenderModel(doValidation) { var valueIds = $scope.model.value ? $scope.model.value.split(',') : []; //sync the sortable model $scope.sortableModel = valueIds; + removeAllEntriesAction.isDisabled = valueIds.length === 0; //load current data if anything selected if (valueIds.length > 0) { //need to determine which items we already have loaded @@ -16353,7 +18146,7 @@ if (entityType !== 'Member') { entityResource.getUrl(entity.id, entityType).then(function (data) { // update url - angular.forEach($scope.renderModel, function (item) { + $scope.renderModel.forEach(function (item) { if (item.id === entity.id) { if (entity.trashed) { item.url = vm.labels.general_recycleBin; @@ -16391,6 +18184,7 @@ 'icon': item.icon, 'path': item.path, 'url': item.url, + 'key': item.key, '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 }); @@ -16404,7 +18198,42 @@ $scope.sortableOptions.disabled = true; } } + function removeAllEntries() { + localizationService.localizeMany([ + 'content_nestedContentDeleteAllItems', + 'general_delete' + ]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function close() { + overlayService.close(); + }, + submit: function submit() { + $scope.clear(); + overlayService.close(); + } + }); + }); + } function init() { + userService.getCurrentUser().then(function (user) { + switch (entityType) { + case 'Document': + var hasAccessToContent = user.allowedSections.indexOf('content') !== -1; + $scope.allowOpenButton = hasAccessToContent; + break; + case 'Media': + var hasAccessToMedia = user.allowedSections.indexOf('media') !== -1; + $scope.allowOpenButton = hasAccessToMedia; + break; + case 'Member': + var hasAccessToMember = user.allowedSections.indexOf('member') !== -1; + $scope.allowOpenButton = hasAccessToMember; + break; + default: + } + }); localizationService.localizeMany([ 'general_recycleBin', 'general_add' @@ -16444,7 +18273,8 @@ } }; // map the user config - $scope.model.config = angular.extend(config, $scope.model.config); + $scope.model.config = Utilities.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'; @@ -16468,7 +18298,9 @@ time_24hr: true }; // Don't show calendar if date format has been set to only time - if ($scope.model.config.format === 'HH:mm:ss' || $scope.model.config.format === 'HH:mm' || $scope.model.config.format === 'HH') { + var timeFormat = $scope.model.config.format.toLowerCase(); + var timeFormatPattern = /^h{1,2}:m{1,2}:s{1,2}\s?a?$/gmi; + if (timeFormat.match(timeFormatPattern)) { $scope.datePickerConfig.enableTime = true; $scope.datePickerConfig.noCalendar = true; } @@ -16558,7 +18390,12 @@ } else { $scope.model.value = null; } - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); + } + function setDirty() { + if ($scope.datePickerForm) { + $scope.datePickerForm.datepicker.$setDirty(); + } } /** Sets the value of the date picker control adn associated viewModel objects based on the model value */ function setDatePickerVal() { @@ -16595,11 +18432,17 @@ multiple: false }; //map the user config - angular.extend(config, $scope.model.config); + Utilities.extend(config, $scope.model.config); //map back to the model $scope.model.config = config; //ensure this is a bool, old data could store zeros/ones or string versions $scope.model.config.multiple = Object.toBoolean($scope.model.config.multiple); + //ensure when form is saved that we don't store [] or [null] as string values in the database when no items are selected + $scope.$on('formSubmitting', function () { + if ($scope.model.value && ($scope.model.value.length === 0 || $scope.model.value[0] === null)) { + $scope.model.value = null; + } + }); function convertArrayToDictionaryArray(model) { //now we need to format the items in the dictionary because we always want to have an array var newItems = []; @@ -16630,13 +18473,13 @@ $scope.updateSingleDropdownValue = function () { $scope.model.value = [$scope.model.singleDropdownValue]; }; - if (angular.isArray($scope.model.config.items)) { + if (Utilities.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])) { + if (!Utilities.isObject($scope.model.config.items[0])) { $scope.model.config.items = convertArrayToDictionaryArray($scope.model.config.items); } - } else if (angular.isObject($scope.model.config.items)) { + } else if (Utilities.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'; @@ -16718,7 +18561,40 @@ } } } - angular.module('umbraco').controller('Umbraco.PropertyEditors.EntityPickerController', entityPicker); + angular.module('umbraco').controller('Umbraco.PropertyEditors.EntityPickerController', entityPicker); + 'use strict'; + function EyeDropperColorPickerController($scope, angularHelper) { + var vm = this; + //setup the default config + var config = { + showAlpha: true, + showPalette: true, + allowEmpty: true + }; + // map the user config + Utilities.extend(config, $scope.model.config); + // map back to the model + $scope.model.config = config; + vm.options = $scope.model.config; + vm.color = $scope.model.value || null; + vm.selectColor = function (color) { + angularHelper.safeApply($scope, function () { + vm.color = color ? color.toString() : null; + $scope.model.value = vm.color; + $scope.propertyForm.selectedColor.$setViewValue(vm.color); + }); + }; + // 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 != ''; + return { + isValid: isValid, + errorMsg: $scope.model.validation.mandatoryMessage || 'Value cannot be empty', + errorKey: 'required' + }; + }; + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.EyeDropperColorPickerController', EyeDropperColorPickerController); 'use strict'; (function () { 'use strict'; @@ -16735,6 +18611,9 @@ $scope.fileChanged = onFileChanged; //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; + $scope.fileExtensionsString = $scope.model.config.fileExtensions ? $scope.model.config.fileExtensions.map(function (x) { + return '.' + x.value; + }).join(',') : ''; /** * Called when the file selection value changes * @param {any} value @@ -16752,12 +18631,13 @@ fileManager.setFiles({ propertyAlias: $scope.model.alias, culture: $scope.model.culture, + segment: $scope.model.segment, files: [] }); } } ; - angular.module('umbraco').controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController).run(function (mediaHelper, umbRequestHelper, assetsService) { + angular.module('umbraco').controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController).run(function (mediaHelper) { 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. @@ -16816,13 +18696,67 @@ $scope.model.close(); } } + vm.showEmptyState = false; + vm.showConfig = false; + vm.showStyles = false; + $scope.$watchCollection('model.config', onWatch); + $scope.$watchCollection('model.styles', onWatch); + function onWatch() { + vm.showConfig = $scope.model.config && ($scope.model.config.length > 0 || Object.keys($scope.model.config).length > 0); + vm.showStyles = $scope.model.styles && ($scope.model.styles.length > 0 || Object.keys($scope.model.styles).length > 0); + vm.showEmptyState = vm.showConfig === false && vm.showStyles === false; + } } angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditor.ConfigController', ConfigController); 'use strict'; - function EditConfigController($scope) { + function EditConfigController($scope, angularHelper) { var vm = this; vm.submit = submit; vm.close = close; + vm.aceOption = { + mode: 'json', + theme: 'chrome', + showPrintMargin: false, + advanced: { + fontSize: '14px', + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: false + }, + onLoad: function onLoad(_editor) { + vm.editor = _editor; + vm.configJson = Utilities.toJson($scope.model.config, true); + vm.editor.setValue(vm.configJson); + vm.editor.on('blur', blurAceEditor); + } + }; + function blurAceEditor(event, _editor) { + var code = _editor.getValue(); + //var form = angularHelper.getCurrentForm($scope); + var form = vm.gridConfigEditor; + var isValid = isValidJson(code); + if (isValid) { + $scope.model.config = Utilities.fromJson(code); + setValid(form); + } else { + setInvalid(form); + } + } + function isValidJson(model) { + var flag = true; + try { + Utilities.fromJson(model); + } catch (err) { + flag = false; + } + return flag; + } + function setValid(form) { + form.$setValidity('json', true); + } + function setInvalid(form) { + form.$setValidity('json', false); + } function submit() { if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); @@ -16837,71 +18771,122 @@ angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditor.EditConfigController', EditConfigController); 'use strict'; angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditor.LayoutConfigController', function ($scope, localizationService) { + var vm = this; + vm.toggleAllowed = toggleAllowed; + vm.configureSection = configureSection; + vm.deleteSection = deleteSection; + vm.selectRow = selectRow; + vm.percentage = percentage; + vm.scaleUp = scaleUp; + vm.scaleDown = scaleDown; + vm.close = close; + vm.submit = submit; + vm.labels = {}; function init() { - setTitle(); + $scope.currentLayout = $scope.model.currentLayout; + $scope.columns = $scope.model.columns; + $scope.rows = $scope.model.rows; + $scope.currentSection = null; + // Setup copy of rows on sections + if ($scope.currentLayout && $scope.currentLayout.sections) { + $scope.currentLayout.sections.forEach(function (section) { + section.rows = Utilities.copy($scope.rows); + // Check if rows are selected + section.rows.forEach(function (row) { + row.selected = section.allowed && section.allowed.includes(row.name); + }); + }); + } + var labelKeys = [ + 'grid_addGridLayout', + 'grid_allowAllRowConfigurations' + ]; + localizationService.localizeMany(labelKeys).then(function (data) { + vm.labels.title = data[0]; + vm.labels.allowAllRowConfigurations = data[1]; + setTitle(vm.labels.title); + }); } - function setTitle() { + function setTitle(value) { if (!$scope.model.title) { - localizationService.localize('grid_addGridLayout').then(function (data) { - $scope.model.title = data; - }); + $scope.model.title = value; } } - $scope.currentLayout = $scope.model.currentLayout; - $scope.columns = $scope.model.columns; - $scope.rows = $scope.model.rows; - $scope.currentSection = undefined; - $scope.scaleUp = function (section, max, overflow) { + function scaleUp(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) { + } + function scaleDown(section) { var remove = section.grid > 1 ? 1 : 0; section.grid = section.grid - remove; - }; - $scope.percentage = function (spans) { + } + function percentage(spans) { return (spans / $scope.columns * 100).toFixed(8); - }; + } /**************** Section *****************/ - $scope.configureSection = function (section, template) { - if (section === undefined) { + function configureSection(section, template) { + if (section === null || section === undefined) { var space = $scope.availableLayoutSpace > 4 ? 4 : $scope.availableLayoutSpace; - section = { grid: space }; + section = { + grid: space, + rows: Utilities.copy($scope.rows) + }; template.sections.push(section); } + section.allowAll = section.allowAll || !section.allowed || !section.allowed.length; $scope.currentSection = section; - $scope.currentSection.allowAll = section.allowAll || !section.allowed || !section.allowed.length; - }; - $scope.toggleAllowed = function (section) { + } + function toggleAllowed(section) { + section.allowAll = !section.allowAll; if (section.allowed) { delete section.allowed; } else { section.allowed = []; } - }; - $scope.deleteSection = function (section, template) { + } + function deleteSection(section, template) { if ($scope.currentSection === section) { - $scope.currentSection = undefined; + $scope.currentSection = null; } var index = template.sections.indexOf(section); template.sections.splice(index, 1); - }; - $scope.close = function () { + } + function selectRow(section, row) { + section.allowed = section.allowed || []; + var index = section.allowed.indexOf(row.name); + if (row.selected === true) { + if (index === -1) { + section.allowed.push(row.name); + } + } else { + section.allowed.splice(index, 1); + } + } + function close() { if ($scope.model.close) { + cleanUpRows(); $scope.model.close(); } - }; - $scope.submit = function () { + } + function submit() { if ($scope.model.submit) { + cleanUpRows(); $scope.model.submit($scope.currentLayout); } - }; + } + function cleanUpRows() { + $scope.currentLayout.sections.forEach(function (section) { + if (section.rows) { + delete section.rows; + } + }); + } $scope.$watch('currentLayout', function (layout) { if (layout) { var total = 0; @@ -16915,42 +18900,62 @@ }); 'use strict'; function RowConfigController($scope, localizationService) { + var vm = this; + vm.configureCell = configureCell; + vm.closeArea = closeArea; + vm.deleteArea = deleteArea; + vm.selectEditor = selectEditor; + vm.toggleAllowed = toggleAllowed; + vm.percentage = percentage; + vm.scaleUp = scaleUp; + vm.scaleDown = scaleDown; + vm.close = close; + vm.submit = submit; + vm.labels = {}; function init() { - setTitle(); + $scope.currentRow = $scope.model.currentRow; + $scope.columns = $scope.model.columns; + $scope.editors = $scope.model.editors; + $scope.nameChanged = false; + var labelKeys = [ + 'grid_addRowConfiguration', + 'grid_allowAllEditors' + ]; + localizationService.localizeMany(labelKeys).then(function (data) { + vm.labels.title = data[0]; + vm.labels.allowAllEditors = data[1]; + setTitle(vm.labels.title); + }); } - function setTitle() { + function setTitle(value) { if (!$scope.model.title) { - localizationService.localize('grid_addRowConfiguration').then(function (data) { - $scope.model.title = data; - }); + $scope.model.title = value; } } - $scope.currentRow = $scope.model.currentRow; - $scope.editors = $scope.model.editors; - $scope.columns = $scope.model.columns; - $scope.scaleUp = function (section, max, overflow) { + function scaleUp(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) { + } + ; + function scaleDown(section) { var remove = section.grid > 1 ? 1 : 0; section.grid = section.grid - remove; - }; - $scope.percentage = function (spans) { + } + function percentage(spans) { return (spans / $scope.columns * 100).toFixed(8); - }; + } /**************** area *****************/ - $scope.configureCell = function (cell, row) { + function configureCell(cell, row) { if ($scope.currentCell && $scope.currentCell === cell) { delete $scope.currentCell; } else { - if (cell === undefined) { + if (cell === null) { var available = $scope.availableRowSpace; var space = 4; if (available < 4 && available > 0) { @@ -16959,39 +18964,54 @@ cell = { grid: space }; row.areas.push(cell); } + cell.allowed = cell.allowed || []; + $scope.editors.forEach(function (e) { + e.allowed = cell.allowed.indexOf(e.alias) !== -1; + }); + cell.allowAll = cell.allowAll || !cell.allowed || !cell.allowed.length; $scope.currentCell = cell; - $scope.currentCell.allowAll = cell.allowAll || !cell.allowed || !cell.allowed.length; } - }; - $scope.toggleAllowed = function (cell) { + } + function toggleAllowed(cell) { + cell.allowAll = !cell.allowAll; if (cell.allowed) { delete cell.allowed; } else { cell.allowed = []; } - }; - $scope.deleteArea = function (cell, row) { + } + function deleteArea(cell, row) { if ($scope.currentCell === cell) { - $scope.currentCell = undefined; + $scope.currentCell = null; } var index = row.areas.indexOf(cell); row.areas.splice(index, 1); - }; - $scope.closeArea = function () { - $scope.currentCell = undefined; - }; - $scope.close = function () { + } + // This doesn't seem to be used? + function closeArea() { + $scope.currentCell = null; + } + function selectEditor(cell, editor) { + cell.allowed = cell.allowed || []; + var index = cell.allowed.indexOf(editor.alias); + if (editor.allowed === true) { + if (index === -1) { + cell.allowed.push(editor.alias); + } + } else { + cell.allowed.splice(index, 1); + } + } + function close() { if ($scope.model.close) { $scope.model.close(); } - }; - $scope.submit = function () { + } + function submit() { if ($scope.model.submit) { $scope.model.submit($scope.currentRow); } - }; - $scope.nameChanged = false; - var originalName = $scope.currentRow.name; + } $scope.$watch('currentRow', function (row) { if (row) { var total = 0; @@ -16999,6 +19019,7 @@ total = total + area.grid; }); $scope.availableRowSpace = $scope.columns - total; + var originalName = $scope.currentRow.name; if (originalName) { if (originalName != row.name) { $scope.nameChanged = true; @@ -17012,23 +19033,10 @@ } angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditor.RowConfigController', RowConfigController); 'use strict'; - function DeleteRowConfirmController($scope) { - $scope.close = function () { - if ($scope.model.close) { - $scope.model.close(); - } - }; - $scope.submit = function () { - if ($scope.model && $scope.model.submit) { - $scope.model.submit($scope.model); - } - }; - } - angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditor.DeleteRowConfirmController', DeleteRowConfirmController); - 'use strict'; angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.EmbedController', function ($scope, $timeout, $sce, editorService) { function onInit() { - var embedPreview = angular.isObject($scope.control.value) && $scope.control.value.preview ? $scope.control.value.preview : $scope.control.value; + $scope.control.icon = $scope.control.icon || 'icon-movie-alt'; + var embedPreview = Utilities.isObject($scope.control.value) && $scope.control.value.preview ? $scope.control.value.preview : $scope.control.value; $scope.trustedValue = embedPreview ? $sce.trustAsHtml(embedPreview) : null; if (!$scope.control.value) { $timeout(function () { @@ -17039,9 +19047,9 @@ } } $scope.setEmbed = function () { - var original = angular.isObject($scope.control.value) ? $scope.control.value : null; + var modify = Utilities.isObject($scope.control.value) ? $scope.control.value : null; var embed = { - original: original, + modify: modify, submit: function submit(model) { var embed = { constrain: model.embed.constrain, @@ -17064,8 +19072,11 @@ onInit(); }); 'use strict'; - angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.MacroController', function ($scope, $timeout, editorService, macroResource, macroService, $routeParams) { - $scope.title = 'Click to insert macro'; + angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.MacroController', function ($scope, $timeout, editorService, macroResource, macroService, localizationService, $routeParams) { + $scope.control.icon = $scope.control.icon || 'icon-settings-alt'; + localizationService.localize('grid_clickToInsertMacro').then(function (label) { + $scope.title = label; + }); $scope.setMacro = function () { var dialogData = { richTextEditor: true, @@ -17106,7 +19117,8 @@ }, 200); }); 'use strict'; - angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.MediaController', function ($scope, $timeout, userService, editorService) { + angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.MediaController', function ($scope, userService, editorService, localizationService) { + $scope.control.icon = $scope.control.icon || 'icon-picture'; $scope.thumbnailUrl = getThumbnailUrl(); if (!$scope.model.config.startNodeId) { if ($scope.model.config.ignoreUserStartNodes === true) { @@ -17120,60 +19132,94 @@ } } $scope.setImage = function () { - var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; - var startNodeIsVirtual = startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; + var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : null; var mediaPicker = { startNodeId: startNodeId, - startNodeIsVirtual: startNodeIsVirtual, - cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined, + startNodeIsVirtual: startNodeId ? $scope.model.config.startNodeIsVirtual : null, + cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : null, showDetails: true, disableFolderSelect: true, onlyImages: true, dataTypeKey: $scope.model.dataTypeKey, submit: function submit(model) { - var selectedImage = model.selection[0]; - $scope.control.value = { - focalPoint: selectedImage.focalPoint, - id: selectedImage.id, - udi: selectedImage.udi, - image: selectedImage.image, - caption: selectedImage.altText - }; + updateControlValue(model.selection[0]); editorService.close(); }, close: function close() { - editorService.close(); + return editorService.close(); } }; editorService.mediaPicker(mediaPicker); }; - $scope.$watch('control.value', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) { - return; // simply skip that - } - $scope.thumbnailUrl = getThumbnailUrl(); - }, true); + $scope.editImage = function () { + var mediaCropDetailsConfig = { + size: 'small', + target: $scope.control.value, + submit: function submit(model) { + updateControlValue(model.target); + editorService.close(); + }, + close: function close() { + return editorService.close(); + } + }; + localizationService.localize('defaultdialogs_editSelectedMedia').then(function (value) { + mediaCropDetailsConfig.title = value; + editorService.mediaCropDetails(mediaCropDetailsConfig); + }); + }; + /** + * + */ function getThumbnailUrl() { if ($scope.control.value && $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; + if ($scope.control.value.coordinates) { + // New way, crop by percent must come before width/height. + var coords = $scope.control.value.coordinates; + url += '?crop='.concat(coords.x1, ',').concat(coords.y1, ',').concat(coords.x2, ',').concat(coords.y2, '&cropmode=percentage'); + } else { + // Here in order not to break existing content where focalPoint were used. + // For some reason width/height have to come first when mode=crop. + if ($scope.control.value.focalPoint) { + url += '?center='.concat($scope.control.value.focalPoint.top, ',').concat($scope.control.value.focalPoint.left); + url += '&mode=crop'; + } else { + // Prevent black padding and no crop when focal point not set / changed from default + url += '?center=0.5,0.5&mode=crop'; + } + } + 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) { + if (url.includes('?') === false) { url += '?width=800&upscale=false&animationprocessmode=false'; } return url; } return null; } - ; + /** + * + * @param {object} selectedImage + */ + function updateControlValue(selectedImage) { + // we could apply selectedImage directly to $scope.control.value, + // but this allows excluding fields in future if needed + $scope.control.value = { + focalPoint: selectedImage.focalPoint, + coordinates: selectedImage.coordinates, + id: selectedImage.id, + udi: selectedImage.udi, + image: selectedImage.image, + caption: selectedImage.caption, + altText: selectedImage.altText + }; + $scope.thumbnailUrl = getThumbnailUrl(); + } }); 'use strict'; (function () { @@ -17187,7 +19233,7 @@ angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.TextStringController', function () { }); 'use strict'; - angular.module('umbraco').controller('Umbraco.PropertyEditors.GridController', function ($scope, localizationService, gridService, umbRequestHelper, angularHelper, $element, eventsService, editorService, $interpolate) { + angular.module('umbraco').controller('Umbraco.PropertyEditors.GridController', function ($scope, localizationService, gridService, umbRequestHelper, angularHelper, $element, eventsService, editorService, overlayService, $interpolate) { // Grid status variables var placeHolder = ''; var currentForm = angularHelper.getCurrentForm($scope); @@ -17300,8 +19346,10 @@ }, over: function over(event, ui) { var area = event.target.getScope_HackForSortable().area; - var allowedEditors = area.allowed; - if ($.inArray(ui.item[0].getScope_HackForSortable().control.editor.alias, allowedEditors) < 0 && allowedEditors || startingArea != area && area.maxItems != '' && area.maxItems > 0 && area.maxItems < area.controls.length + 1) { + var allowedEditors = area.$allowedEditors.map(function (e) { + return e.alias; + }); + if ($.inArray(ui.item[0].getScope_HackForSortable().control.editor.alias, allowedEditors) < 0 || startingArea != area && area.maxItems != '' && area.maxItems > 0 && area.maxItems < area.controls.length + 1) { $scope.$apply(function () { area.dropNotAllowed = true; }); @@ -17439,31 +19487,31 @@ // Add items overlay menu // ********************************************* $scope.openEditorOverlay = function (event, area, index, key) { - var title = ''; - localizationService.localize('grid_insertControl').then(function (value) { - title = value; - $scope.editorOverlay = { - view: 'itempicker', - filter: area.$allowedEditors.length > 15, - title: title, - availableItems: area.$allowedEditors, - event: event, - show: true, - submit: function submit(model) { - if (model.selectedItem) { - $scope.addControl(model.selectedItem, area, index); - $scope.editorOverlay.show = false; - $scope.editorOverlay = null; - } + var dialog = { + view: 'itempicker', + filter: area.$allowedEditors.length > 15, + availableItems: area.$allowedEditors, + event: event, + submit: function submit(model) { + if (model.selectedItem) { + $scope.addControl(model.selectedItem, area, index); + overlayService.close(); } - }; + }, + close: function close() { + overlayService.close(); + } + }; + localizationService.localize('grid_insertControl').then(function (value) { + dialog.title = value; + overlayService.open(dialog); }); }; // ********************************************* // Template management functions // ********************************************* $scope.addTemplate = function (template) { - $scope.model.value = angular.copy(template); + $scope.model.value = Utilities.copy(template); //default row data _.forEach($scope.model.value.sections, function (section) { $scope.initSection(section); @@ -17491,7 +19539,7 @@ } $scope.addRow = function (section, layout, isInit) { //copy the selected layout into the rows collection - var row = angular.copy(layout); + var row = Utilities.copy(layout); // Init row value row = $scope.initRow(row); // Push the new row @@ -17507,13 +19555,15 @@ element: $element, row: row }); - // TODO: find a nicer way to do this without relying on setTimeout - setTimeout(function () { - var newRowEl = $element.find('[data-rowid=\'' + row.$uniqueId + '\']'); - if (newRowEl !== null) { - newRowEl.focus(); - } - }, 0); + if (!isInit) { + // TODO: find a nicer way to do this without relying on setTimeout + setTimeout(function () { + var newRowEl = $element.find('[data-rowid=\'' + row.$uniqueId + '\']'); + if (newRowEl !== null) { + newRowEl.focus(); + } + }, 0); + } }; $scope.removeRow = function (section, $index) { if (section.rows.length > 0) { @@ -17560,16 +19610,16 @@ var styles, config; if (itemType === 'control') { styles = null; - config = angular.copy(gridItem.editor.config.settings); + config = Utilities.copy(gridItem.editor.config.settings); } else { - styles = _.filter(angular.copy($scope.model.config.items.styles), function (item) { + styles = _.filter(Utilities.copy($scope.model.config.items.styles), function (item) { return shouldApply(item, itemType, gridItem); }); - config = _.filter(angular.copy($scope.model.config.items.config), function (item) { + config = _.filter(Utilities.copy($scope.model.config.items.config), function (item) { return shouldApply(item, itemType, gridItem); }); } - if (angular.isObject(gridItem.config)) { + if (Utilities.isObject(gridItem.config)) { _.each(config, function (cfg) { var val = gridItem.config[cfg.key]; if (val) { @@ -17577,7 +19627,7 @@ } }); } - if (angular.isObject(gridItem.styles)) { + if (Utilities.isObject(gridItem.styles)) { _.each(styles, function (style) { var val = gridItem.styles[style.key]; if (val) { @@ -17690,16 +19740,8 @@ 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.setUniqueId = function () { + return String.CreateGuid(); }; $scope.addControl = function (editor, cell, index, initialize) { initialize = initialize !== false; @@ -17784,11 +19826,11 @@ //if nothing is found, set it to 12 if (!$scope.model.config.items.columns) { $scope.model.config.items.columns = 12; - } else if (angular.isString($scope.model.config.items.columns)) { + } else if (Utilities.isString($scope.model.config.items.columns)) { $scope.model.config.items.columns = parseInt($scope.model.config.items.columns); } 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)) { + if ($scope.model.value.name && Utilities.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 @@ -17796,12 +19838,12 @@ 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) { + if (found && Utilities.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)); + Utilities.extend($scope.model.value.sections[index], Utilities.copy(templateSection)); }); } } @@ -17861,7 +19903,7 @@ return null; } else { //make a copy to not touch the original config - original = angular.copy(original); + original = Utilities.copy(original); original.styles = row.styles; original.config = row.config; original.hasConfig = gridItemHasConfig(row.styles, row.config); @@ -17946,7 +19988,7 @@ gridService.getGridEditors().then(function (response) { $scope.availableEditors = response.data; //Localize the grid editor names - angular.forEach($scope.availableEditors, function (value, key) { + $scope.availableEditors.forEach(function (value) { //If no translation is provided, keep using the editor name from the manifest localizationService.localize('grid_' + value.alias, undefined, value.name).then(function (v) { value.name = v; @@ -18024,7 +20066,18 @@ angular.module('umbraco').directive('umbGridHackScope', umbGridHackScope); }()); 'use strict'; - angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditorController', function ($scope, gridService, editorService) { + angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditorController', function ($scope, gridService, editorService, localizationService, overlayService) { + var vm = this; + vm.configureTemplate = configureTemplate; + vm.deleteTemplate = deleteTemplate; + vm.configureLayout = configureLayout; + vm.deleteLayout = deleteLayout; + vm.toggleCollection = toggleCollection; + vm.percentage = percentage; + vm.zeroWidthFilter = zeroWidthFilter; + vm.removeConfigValue = removeConfigValue; + vm.editConfig = editConfig; + vm.editStyles = editStyles; var emptyModel = { styles: [{ label: 'Set a background image', @@ -18073,9 +20126,9 @@ ] }; /**************** - template + Template *****************/ - $scope.configureTemplate = function (template) { + function configureTemplate(template) { var index = $scope.model.value.templates.indexOf(template); if (template === undefined) { template = { @@ -18084,7 +20137,7 @@ }; } var layoutConfigOverlay = { - currentLayout: angular.copy(template), + currentLayout: Utilities.copy(template), rows: $scope.model.value.layouts, columns: $scope.model.value.columns, view: 'views/propertyEditors/grid/dialogs/layoutconfig.html', @@ -18102,14 +20155,14 @@ } }; editorService.open(layoutConfigOverlay); - }; - $scope.deleteTemplate = function (index) { + } + function deleteTemplate(index) { $scope.model.value.templates.splice(index, 1); - }; + } /**************** Row *****************/ - $scope.configureLayout = function (layout) { + function configureLayout(layout) { var index = $scope.model.value.layouts.indexOf(layout); if (layout === undefined) { layout = { @@ -18118,7 +20171,7 @@ }; } var rowConfigOverlay = { - currentRow: angular.copy(layout), + currentRow: Utilities.copy(layout), editors: $scope.editors, columns: $scope.model.value.columns, view: 'views/propertyEditors/grid/dialogs/rowconfig.html', @@ -18136,45 +20189,51 @@ } }; editorService.open(rowConfigOverlay); - }; - //var rowDeletesPending = false; - $scope.deleteLayout = function (index) { - var rowDeleteOverlay = { - dialogData: { rowName: $scope.model.value.layouts[index].name }, - view: 'views/propertyEditors/grid/dialogs/rowdeleteconfirm.html', - size: 'small', + } + function deleteLayout(layout, index, event) { + var dialog = { + view: 'views/propertyEditors/grid/overlays/rowdeleteconfirm.html', + layout: layout, + submitButtonLabelKey: 'contentTypeEditor_yesDelete', + submitButtonStyle: 'danger', submit: function submit(model) { $scope.model.value.layouts.splice(index, 1); - editorService.close(); + overlayService.close(); }, - close: function close(model) { - editorService.close(); + close: function close() { + overlayService.close(); } }; - editorService.open(rowDeleteOverlay); - }; + localizationService.localize('general_delete').then(function (value) { + dialog.title = value; + overlayService.open(dialog); + }); + event.preventDefault(); + event.stopPropagation(); + } /**************** - utillities + Utilities *****************/ - $scope.toggleCollection = function (collection, toggle) { + function toggleCollection(collection, toggle) { if (toggle) { collection = []; } else { collection = null; } - }; - $scope.percentage = function (spans) { + } + ; + function percentage(spans) { return (spans / $scope.model.value.columns * 100).toFixed(8); - }; - $scope.zeroWidthFilter = function (cell) { + } + function zeroWidthFilter(cell) { return cell.grid > 0; - }; + } /**************** Config *****************/ - $scope.removeConfigValue = function (collection, index) { + function removeConfigValue(collection, index) { collection.splice(index, 1); - }; + } var editConfigCollection = function editConfigCollection(configValues, title, callback) { var editConfigCollectionOverlay = { config: configValues, @@ -18191,23 +20250,23 @@ }; editorService.open(editConfigCollectionOverlay); }; - $scope.editConfig = function () { + function editConfig() { editConfigCollection($scope.model.value.config, 'Settings', function (data) { $scope.model.value.config = data; }); - }; - $scope.editStyles = function () { + } + function editStyles() { editConfigCollection($scope.model.value.styles, 'Styling', function (data) { $scope.model.value.styles = data; }); - }; + } /**************** - editors + Editors *****************/ gridService.getGridEditors().then(function (response) { $scope.editors = response.data; }); - /* init grid data */ + /* Init grid data */ if (!$scope.model.value || $scope.model.value === '' || !$scope.model.value.templates) { $scope.model.value = emptyModel; } else { @@ -18271,8 +20330,8 @@ } angular.module('umbraco').controller('Umbraco.PropertyEditors.IdWithGuidValueController', IdWithGuidValueController); 'use strict'; - angular.module('umbraco').controller('Umbraco.PropertyEditors.ImageCropperController', function ($scope, fileManager, $timeout) { - var config = angular.copy($scope.model.config); + angular.module('umbraco').controller('Umbraco.PropertyEditors.ImageCropperController', function ($scope, fileManager, $timeout, mediaHelper) { + var config = Utilities.copy($scope.model.config); $scope.filesSelected = onFileSelected; $scope.filesChanged = onFilesChanged; $scope.fileUploaderInit = onFileUploaderInit; @@ -18282,9 +20341,12 @@ $scope.clear = clear; $scope.reset = reset; $scope.close = close; + $scope.isCustomCrop = isCustomCrop; $scope.focalPointChanged = focalPointChanged; //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; + $scope.acceptFileExt = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); /** * Called when the umgImageGravity component updates the focal point value * @param {any} left @@ -18297,7 +20359,7 @@ top: top }; //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } /** * Used to assign a new model value @@ -18306,7 +20368,7 @@ function setModelValueWithSrc(src) { if (!$scope.model.value || !$scope.model.value.src) { //we are copying to not overwrite the original config - $scope.model.value = angular.extend(angular.copy($scope.model.config), { src: src }); + $scope.model.value = Utilities.extend(Utilities.copy($scope.model.config), { src: src }); } } /** @@ -18319,6 +20381,7 @@ fileManager.setFiles({ propertyAlias: $scope.model.alias, culture: $scope.model.culture, + segment: $scope.model.segment, files: [] }); } @@ -18329,7 +20392,12 @@ function onFileSelected(value, files) { setModelValueWithSrc(value); //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); + } + function setDirty() { + if ($scope.imageCropperForm) { + $scope.imageCropperForm.modelValue.$setDirty(); + } } function imageLoaded(isCroppable, hasDimensions) { $scope.isCroppable = isCroppable; @@ -18345,7 +20413,7 @@ if (files && files[0]) { $scope.imageSrc = files[0].fileSrc; //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } } /** @@ -18356,7 +20424,7 @@ //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)) { + if (Utilities.isString($scope.model.value)) { setModelValueWithSrc($scope.model.value); } else { //sync any config changes with the editor and drop outdated crops @@ -18392,14 +20460,14 @@ function crop(targetCrop) { if (!$scope.currentCrop) { // clone the crop so we can discard the changes - $scope.currentCrop = angular.copy(targetCrop); + $scope.currentCrop = Utilities.copy(targetCrop); $scope.currentPoint = null; //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } else { // we have a crop open already - close the crop (this will discard any changes made) close(); - // the crop editor needs a digest cycle to close down properly, otherwise its state + // the crop editor needs a digest cycle to close down properly, otherwise its state // is reused for the new crop... and that's really bad $timeout(function () { crop(targetCrop); @@ -18423,7 +20491,7 @@ editedCrop.coordinates = $scope.currentCrop.coordinates; $scope.close(); //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } ; function reset() { @@ -18443,6 +20511,7 @@ fileManager.setFiles({ propertyAlias: $scope.model.alias, culture: $scope.model.culture, + segment: $scope.model.segment, files: [] }); //clear the ui @@ -18451,9 +20520,12 @@ $scope.model.value = null; } //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } ; + function isCustomCrop(crop) { + return !!crop.coordinates; + } var unsubscribe = $scope.$on('formSubmitting', function () { $scope.currentCrop = null; $scope.currentPoint = null; @@ -18472,7 +20544,7 @@ } 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)) { + } else if (Utilities.isString(property.value)) { if (thumbnail) { if (mediaHelper.detectIfImageByExtension(property.value)) { var thumbnailUrl = umbRequestHelper.getApiUrl('imagesApiBaseUrl', 'GetBigThumbnail', [{ originalImagePath: property.value }]); @@ -18524,7 +20596,7 @@ evt.preventDefault(); $scope.editMode = false; $scope.setFocus = true; - if ($scope.newItem && $scope.newItem.alias && angular.isNumber($scope.newItem.width) && angular.isNumber($scope.newItem.height) && $scope.newItem.width > 0 && $scope.newItem.height > 0) { + if ($scope.newItem && $scope.newItem.alias && Utilities.isNumber($scope.newItem.width) && Utilities.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; }); @@ -18543,24 +20615,39 @@ //there was an error, do the highlight (will be set back by the directive) $scope.hasError = true; }; - $scope.sortableOptions = { axis: 'y' }; + $scope.createNew = function (event) { + if (event.keyCode == 13) { + $scope.add(event); + } + }; + $scope.sortableOptions = { + axis: 'y', + containment: 'parent', + cursor: 'move', + tolerance: 'pointer' + }; }); 'use strict'; function iconPreValsController($scope, editorService) { if (!$scope.model.value) { $scope.model.value = 'icon-list'; } + var valueArray = $scope.model.value.split(' '); + $scope.icon = valueArray[0]; + $scope.color = valueArray[1]; $scope.openIconPicker = function () { var iconPicker = { - icon: $scope.model.value.split(' ')[0], - color: $scope.model.value.split(' ')[1], + icon: $scope.icon, + color: $scope.color, submit: function submit(model) { if (model.icon) { if (model.color) { $scope.model.value = model.icon + ' ' + model.color; + $scope.color = model.color; } else { $scope.model.value = model.icon; } + $scope.icon = model.icon; $scope.iconForm.$setDirty(); } editorService.close(); @@ -18624,9 +20711,8 @@ $scope.errorMsg = ''; }; $scope.removeField = function (e) { - $scope.model.value = _.reject($scope.model.value, function (x) { - return x.alias === e.alias; - }); + var index = $scope.model.value.indexOf(e); + $scope.model.value.splice(index, 1); }; //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) { @@ -18739,6 +20825,8 @@ var vm = this; vm.focusLayoutName = false; vm.layoutsSortableOptions = { + axis: 'y', + containment: 'parent', distance: 10, tolerance: 'pointer', opacity: 0.7, @@ -18809,10 +20897,10 @@ var vm = this; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; vm.nodeId = $scope.contentId; - // Use whitelist of allowed file types if provided + // Use list 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 + // If not provided, we pass in a disallowed list by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); } vm.maxFileSize = umbracoSettings.maxFileSize + 'KB'; @@ -18883,7 +20971,7 @@ listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event); } function goToItem(item, $event, $index) { - listViewHelper.editItem(item); + listViewHelper.editItem(item, $scope); } activate(); } @@ -18896,10 +20984,10 @@ var vm = this; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; vm.nodeId = $scope.contentId; - // Use whitelist of allowed file types if provided + // Use list 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 + // If not provided, we pass in a disallowed list by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); } vm.maxFileSize = umbracoSettings.maxFileSize + 'KB'; @@ -18934,7 +21022,7 @@ listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); } function clickItem(item) { - listViewHelper.editItem(item); + listViewHelper.editItem(item, $scope); } function isSortDirection(col, direction) { return listViewHelper.setSortingDirection(col, direction, $scope.options); @@ -18960,15 +21048,17 @@ $scope.getContent($scope.contentId); } function markAsSensitive() { - angular.forEach($scope.options.includeProperties, function (option) { + $scope.options.includeProperties.forEach(function (option) { option.isSensitive = false; - angular.forEach($scope.items, function (item) { - angular.forEach(item.properties, function (property) { - if (option.alias === property.alias) { - option.isSensitive = property.isSensitive; - } + if ($scope.items && $scope.items.length) { + $scope.items.forEach(function (item) { + item.properties.forEach(function (property) { + if (option.alias === property.alias) { + option.isSensitive = property.isSensitive; + } + }); }); - }); + } }); } activate(); @@ -18976,7 +21066,7 @@ angular.module('umbraco').controller('Umbraco.PropertyEditors.ListView.ListLayoutController', ListViewListLayoutController); }()); 'use strict'; - function listViewController($scope, $interpolate, $routeParams, $injector, $timeout, currentUserResource, notificationsService, iconHelper, editorState, localizationService, appState, $location, listViewHelper, navigationService, editorService, overlayService, languageResource, mediaHelper) { + function listViewController($scope, $interpolate, $routeParams, $injector, $timeout, currentUserResource, notificationsService, iconHelper, editorState, localizationService, appState, $location, listViewHelper, navigationService, editorService, overlayService, languageResource, mediaHelper, eventsService) { //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 @@ -19102,6 +21192,7 @@ } var listParamsForCurrent = $routeParams.id == $routeParams.list; $scope.options = { + useInfiniteEditor: $scope.model.config.useInfiniteEditor === true, pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, pageNumber: listParamsForCurrent && $routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0 ? $routeParams.page : 1, filter: (listParamsForCurrent && $routeParams.filter ? $routeParams.filter : '').trim(), @@ -19175,11 +21266,7 @@ //check if response is ysod if (err.status && err.status >= 500) { // Open ysod overlay - $scope.ysodOverlay = { - view: 'ysod', - error: err, - show: true - }; + overlayService.ysod(err); } $timeout(function () { $scope.bulkStatus = ''; @@ -19298,7 +21385,7 @@ 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))) + if (!(result.data && Utilities.isArray(result.data.notifications))) showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); }); } @@ -19584,7 +21671,7 @@ } if (e.nameExp) { var newValue = e.nameExp({ value: value }); - if (newValue && (newValue = $.trim(newValue))) { + if (newValue && (newValue = newValue.trim())) { value = newValue; } } @@ -19593,51 +21680,53 @@ }); } function isDate(val) { - if (angular.isString(val)) { + if (Utilities.isString(val)) { return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); } return false; } - function initView() { - var id = $routeParams.id; - if (id === undefined) { - // no ID found in route params - don't list anything as we don't know for sure where we are - return; - } - getContentTypesCallback(id).then(function (listViewAllowedTypes) { - $scope.listViewAllowedTypes = listViewAllowedTypes; - var blueprints = false; - _.each(listViewAllowedTypes, function (allowedType) { - if (_.isEmpty(allowedType.blueprints)) { - // this helps the view understand that there are no blueprints available - allowedType.blueprints = null; - } else { - blueprints = true; - // turn the content type blueprints object into an array of sortable objects for the view - allowedType.blueprints = _.map(_.pairs(allowedType.blueprints || {}), function (pair) { - return { - id: pair[0], - name: pair[1] - }; - }); - } - }); - if (listViewAllowedTypes.length === 1 && blueprints === false) { - $scope.createAllowedButtonSingle = true; - } - if (listViewAllowedTypes.length === 1 && blueprints === true) { - $scope.createAllowedButtonSingleWithBlueprints = true; - } - if (listViewAllowedTypes.length > 1) { - $scope.createAllowedButtonMultiWithBlueprints = true; - } - }); + function initView() { + var id = $routeParams.id; + if (id === undefined) { + // no ID found in route params - don't list anything as we don't know for sure where we are + return; + } $scope.contentId = id; $scope.isTrashed = editorState.current ? editorState.current.trashed : id === '-20' || id === '-21'; $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; $scope.options.allowBulkCopy = $scope.options.allowBulkCopy && !$scope.isTrashed; $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || $scope.options.allowBulkUnpublish || $scope.options.allowBulkCopy || $scope.options.allowBulkMove || $scope.options.allowBulkDelete; + if ($scope.isTrashed === false) { + getContentTypesCallback(id).then(function (listViewAllowedTypes) { + $scope.listViewAllowedTypes = listViewAllowedTypes; + var blueprints = false; + _.each(listViewAllowedTypes, function (allowedType) { + if (_.isEmpty(allowedType.blueprints)) { + // this helps the view understand that there are no blueprints available + allowedType.blueprints = null; + } else { + blueprints = true; + // turn the content type blueprints object into an array of sortable objects for the view + allowedType.blueprints = _.map(_.pairs(allowedType.blueprints || {}), function (pair) { + return { + id: pair[0], + name: pair[1] + }; + }); + } + }); + if (listViewAllowedTypes.length === 1 && blueprints === false) { + $scope.createAllowedButtonSingle = true; + } + if (listViewAllowedTypes.length === 1 && blueprints === true) { + $scope.createAllowedButtonSingleWithBlueprints = true; + } + if (listViewAllowedTypes.length > 1) { + $scope.createAllowedButtonMultiWithBlueprints = true; + } + }); + } $scope.reloadView($scope.contentId); } function getLocalizedKey(alias) { @@ -19672,6 +21761,34 @@ } } function createBlank(entityType, docTypeAlias) { + if ($scope.options.useInfiniteEditor) { + var editorModel = { + create: true, + submit: function submit(model) { + editorService.close(); + $scope.reloadView($scope.contentId); + }, + close: function close() { + editorService.close(); + $scope.reloadView($scope.contentId); + } + }; + if (entityType == 'content') { + editorModel.parentId = $scope.contentId; + editorModel.documentTypeAlias = docTypeAlias; + editorService.contentEditor(editorModel); + return; + } + if (entityType == 'media') { + editorService.mediaEditor(editorModel); + return; + } + if (entityType == 'member') { + editorModel.doctype = docTypeAlias; + editorService.memberEditor(editorModel); + return; + } + } $location.path('/' + entityType + '/' + entityType + '/edit/' + $scope.contentId).search('doctype', docTypeAlias).search('create', 'true'); } function createFromBlueprint(entityType, docTypeAlias, blueprintId) { @@ -19687,6 +21804,19 @@ $scope.createFromBlueprint = createFromBlueprint; $scope.toggleDropdown = toggleDropdown; $scope.leaveDropdown = leaveDropdown; + // if this listview has sort order in it, make sure it is updated when sorting is performed on the current content + if (_.find($scope.options.includeProperties, function (property) { + return property.alias === 'sortOrder'; + })) { + var eventSubscription = eventsService.on('sortCompleted', function (e, args) { + if (parseInt(args.id) === parseInt($scope.contentId)) { + $scope.reloadView($scope.contentId); + } + }); + $scope.$on('$destroy', function () { + eventsService.unsubscribe(eventSubscription); + }); + } //GO! initView(); } @@ -19760,7 +21890,7 @@ }); $scope.model.disableSubmitButton = !firstSelected; if (language.isMandatory) { - angular.forEach($scope.model.languages, function (lang) { + $scope.model.languages.forEach(function (lang) { if (lang !== language) { lang.unpublish = true; lang.disabled = language.unpublish; @@ -19863,7 +21993,7 @@ key: 'general_name' }, { - value: 'VersionDate', + value: 'UpdateDate', key: 'content_updateDate' }, { @@ -19900,23 +22030,14 @@ 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; - } } }); }); + _.each($scope.sortByFields, function (sortByField) { + if (!sortByField.name) { + sortByField.name = '(' + sortByField.value + ')'; + } + }); // Check existing model value is available in list and ensure a value is set var existingValue = findFromSortByFields($scope.model.value); if (existingValue) { @@ -19932,7 +22053,7 @@ angular.module('umbraco').controller('Umbraco.PrevalueEditors.SortByListViewController', sortByPreValsController); 'use strict'; //inject umbracos assetsServce and dialog service - function MarkdownEditorController($scope, $element, assetsService, editorService, angularHelper, $timeout) { + function MarkdownEditorController($scope, $element, assetsService, editorService, $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 === '') { @@ -19958,6 +22079,7 @@ function openLinkPicker(callback) { var linkPicker = { hideTarget: true, + size: $scope.model.config.overlaySize, submit: function submit(model) { callback(model.target.url, model.target.name); editorService.close(); @@ -19968,6 +22090,11 @@ }; editorService.linkPicker(linkPicker); } + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } assetsService.load([ 'lib/markdown/markdown.converter.js', 'lib/markdown/markdown.sanitizer.js', @@ -19999,7 +22126,7 @@ if ($scope.model.value !== $('textarea', $element).val()) { if ($scope.markdownEditorInitComplete) { //only set dirty after init load to avoid "unsaved" dialogue when we don't want it - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } else { $scope.markdownEditorInitComplete = true; } @@ -20016,14 +22143,17 @@ 'use strict'; //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 ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, angularHelper) { + angular.module('umbraco').controller('Umbraco.PropertyEditors.MediaPickerController', function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, overlayService, clipboardService) { var vm = this; vm.labels = {}; vm.labels.deletedItem = ''; vm.add = add; vm.remove = remove; + vm.copyItem = copyItem; vm.editItem = editItem; vm.showAdd = showAdd; + vm.mediaItems = []; + var selectedIds = []; //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; @@ -20031,8 +22161,6 @@ $scope.allowEditMedia = false; $scope.allowAddMedia = false; function setupViewModel() { - $scope.mediaItems = []; - $scope.ids = []; $scope.isMultiPicker = multiPicker; if ($scope.model.value) { var ids = $scope.model.value.split(','); @@ -20051,65 +22179,67 @@ // 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. + medias = ids.map(function (id) { + // 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. + var found = medias.find(function (m) { return m.udi.toString() === id.toString() || m.id.toString() === id.toString(); }); - if (found) { - return found; - } else { - return { - name: vm.labels.deletedItem, - id: $scope.model.config.idType !== 'udi' ? id : null, - udi: $scope.model.config.idType === 'udi' ? id : null, - icon: 'icon-picture', - thumbnail: null, - trashed: true - }; - } + var mediaItem = found || { + name: vm.labels.deletedItem, + id: $scope.model.config.idType !== 'udi' ? id : null, + udi: $scope.model.config.idType === 'udi' ? id : null, + icon: 'icon-picture', + thumbnail: null, + trashed: true + }; + return mediaItem; }); - _.each(medias, function (media, i) { - if (!media.extension && media.id && media.metaData) { - media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); - } - // 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.mediaItems.push(media); - if ($scope.model.config.idType === 'udi') { - $scope.ids.push(media.udi); - } else { - $scope.ids.push(media.id); - } + medias.forEach(function (media) { + return appendMedia(media); }); sync(); }); } } + function appendMedia(media) { + if (!media.extension && media.id && media.metaData) { + media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); + } + // 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); + } + vm.mediaItems.push(media); + if ($scope.model.config.idType === 'udi') { + selectedIds.push(media.udi); + } else { + selectedIds.push(media.id); + } + } function sync() { - $scope.model.value = $scope.ids.join(); - removeAllEntriesAction.isDisabled = $scope.ids.length === 0; + $scope.model.value = selectedIds.join(); + removeAllEntriesAction.isDisabled = selectedIds.length === 0; + copyAllEntriesAction.isDisabled = removeAllEntriesAction.isDisabled; } - ; function setDirty() { - angularHelper.getCurrentForm($scope).$setDirty(); + if (vm.modelValueForm) { + vm.modelValueForm.modelValue.$setDirty(); + } } function reloadUpdatedMediaItems(updatedMediaNodes) { - // because the images can be edited through the media picker we need to + // because the images can be edited through the media picker we need to // reload. We only reload the images that is already picked but has been updated. - // We have to get the entities from the server because the media + // We have to get the entities from the server because the media // can be edited without being selected - _.each($scope.images, function (image, i) { - if (updatedMediaNodes.indexOf(image.udi) !== -1) { - image.loading = true; - entityResource.getById(image.udi, 'media').then(function (mediaEntity) { - angular.extend(image, mediaEntity); - image.thumbnail = mediaHelper.resolveFileFromEntity(image, true); - image.loading = false; + vm.mediaItems.forEach(function (media) { + if (updatedMediaNodes.indexOf(media.udi) !== -1) { + media.loading = true; + entityResource.getById(media.udi, 'Media').then(function (mediaEntity) { + Utilities.extend(media, mediaEntity); + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + media.loading = false; }); } }); @@ -20142,11 +22272,47 @@ }); } function remove(index) { - $scope.mediaItems.splice(index, 1); - $scope.ids.splice(index, 1); + vm.mediaItems.splice(index, 1); + selectedIds.splice(index, 1); sync(); setDirty(); } + function copyAllEntries() { + if ($scope.mediaItems.length > 0) { + // gather aliases + var aliases = $scope.mediaItems.map(function (mediaEntity) { + return mediaEntity.metaData.ContentTypeAlias; + }); + // remove duplicate aliases + aliases = aliases.filter(function (item, index) { + return aliases.indexOf(item) === index; + }); + var data = $scope.mediaItems.map(function (mediaEntity) { + return { 'mediaKey': mediaEntity.key }; + }); + localizationService.localize('clipboard_labelForArrayOfItems', [$scope.model.label]).then(function (localizedLabel) { + clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, data, localizedLabel, 'icon-thumbnail-list', $scope.model.id); + }); + } + } + function copyItem(mediaItem) { + var mediaEntry = {}; + mediaEntry.mediaKey = mediaItem.key; + clipboardService.copy(clipboardService.TYPES.MEDIA, mediaItem.metaData.ContentTypeAlias, mediaEntry, mediaItem.name, mediaItem.icon, mediaItem.udi); + } + function pasteFromClipboard(pasteEntry, pasteType) { + if (pasteEntry === undefined) { + return; + } + pasteEntry = clipboardService.parseContentForPaste(pasteEntry, pasteType); + entityResource.getById(pasteEntry.mediaKey, 'Media').then(function (mediaEntity) { + if (disableFolderSelect === true && mediaEntity.metaData.ContentTypeAlias === 'Folder') { + return; + } + appendMedia(mediaEntity); + sync(); + }); + } function editItem(item) { var mediaEditor = { id: item.id, @@ -20156,19 +22322,19 @@ // the media picker is using media entities so we get the // entity so we easily can format it for use in the media grid if (model && model.mediaNode) { - entityResource.getById(model.mediaNode.id, 'media').then(function (mediaEntity) { - // if an image is selecting more than once + entityResource.getById(model.mediaNode.id, 'Media').then(function (mediaEntity) { + // if an image is selecting more than once // we need to update all the media items - angular.forEach($scope.images, function (image) { - if (image.id === model.mediaNode.id) { - angular.extend(image, mediaEntity); - image.thumbnail = mediaHelper.resolveFileFromEntity(image, true); + vm.mediaItems.forEach(function (media) { + if (media.id === model.mediaNode.id) { + Utilities.extend(media, mediaEntity); + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } }); }); } }, - close: function close(model) { + close: function close() { editorService.close(); } }; @@ -20182,18 +22348,34 @@ multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, + clickPasteItem: function clickPasteItem(item, mouseEvent) { + if (Array.isArray(item.data)) { + var indexIncrementor = 0; + item.data.forEach(function (entry) { + if (pasteFromClipboard(entry, item.type)) { + indexIncrementor++; + } + }); + } else { + pasteFromClipboard(item.data, item.type); + } + if (!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + editorService.close(); + } + setDirty(); + }, submit: function submit(model) { editorService.close(); - _.each(model.selection, function (media, i) { + model.selection.forEach(function (media) { // 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.mediaItems.push(media); + vm.mediaItems.push(media); if ($scope.model.config.idType === 'udi') { - $scope.ids.push(media.udi); + selectedIds.push(media.udi); } else { - $scope.ids.push(media.id); + selectedIds.push(media.id); } }); sync(); @@ -20205,6 +22387,17 @@ reloadUpdatedMediaItems(model.updatedMediaNodes); } }; + var allowedTypes = null; + if (onlyImages) { + allowedTypes = ['Image']; // Media Type Image Alias. + } + mediaPicker.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, allowedTypes); + }; + mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, allowedTypes); + mediaPicker.clipboardItems.sort(function (a, b) { + return b.date - a.date; + }); editorService.mediaPicker(mediaPicker); } function showAdd() { @@ -20216,13 +22409,35 @@ return true; } function removeAllEntries() { - $scope.mediaItems.length = 0; - // AngularJS way to empty the array. - $scope.ids.length = 0; - // AngularJS way to empty the array. - sync(); - setDirty(); + localizationService.localizeMany([ + 'content_nestedContentDeleteAllItems', + 'general_delete' + ]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function close() { + overlayService.close(); + }, + submit: function submit() { + vm.mediaItems.length = 0; + // AngularJS way to empty the array. + selectedIds.length = 0; + // AngularJS way to empty the array. + sync(); + setDirty(); + overlayService.close(); + } + }); + }); } + var copyAllEntriesAction = { + labelKey: 'clipboard_labelForCopyAllEntries', + labelTokens: ['Media'], + icon: 'documents', + method: copyAllEntries, + isDisabled: true + }; var removeAllEntriesAction = { labelKey: 'clipboard_labelForRemoveAllEntries', labelTokens: [], @@ -20231,7 +22446,10 @@ isDisabled: true }; if (multiPicker === true) { - var propertyActions = [removeAllEntriesAction]; + var propertyActions = [ + copyAllEntriesAction, + removeAllEntriesAction + ]; if ($scope.umbProperty) { $scope.umbProperty.setPropertyActions(propertyActions); } @@ -20243,14 +22461,14 @@ disabled: !multiPicker, items: 'li:not(.add-wrapper)', cancel: '.unsortable', - update: function update(e, ui) { + update: function update() { setDirty(); $timeout(function () { // TODO: Instead of doing this with a timeout 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. - $scope.ids = _.map($scope.mediaItems, function (item) { - return $scope.model.config.idType === 'udi' ? item.udi : item.id; + selectedIds = vm.mediaItems.map(function (media) { + return $scope.model.config.idType === 'udi' ? media.udi : media.id; }); sync(); }); @@ -20259,9 +22477,138 @@ init(); }); 'use strict'; + angular.module('umbraco').controller('Umbraco.PropertyEditors.MediaPicker3.CropConfigurationController', function ($scope) { + var unsubscribe = []; + if (!$scope.model.value) { + $scope.model.value = []; + } + $scope.setFocus = false; + $scope.remove = function (crop, evt) { + evt.preventDefault(); + var i = $scope.model.value.indexOf(crop); + if (i > -1) { + $scope.model.value.splice(i, 1); + } + }; + $scope.edit = function (crop, evt) { + evt.preventDefault(); + crop.editMode = true; + }; + $scope.addNewCrop = function (evt) { + evt.preventDefault(); + var crop = {}; + crop.editMode = true; + $scope.model.value.push(crop); + $scope.validate(crop); + }; + $scope.setChanges = function (crop) { + $scope.validate(crop); + if (crop.hasWidthError !== true && crop.hasHeightError !== true && crop.hasAliasError !== true) { + crop.editMode = false; + window.dispatchEvent(new Event('resize.umbImageGravity')); + } + }; + $scope.isEmpty = function (crop) { + return !crop.label && !crop.alias && !crop.width && !crop.height; + }; + $scope.useForAlias = function (crop) { + if (crop.alias == null || crop.alias === '') { + crop.alias = (crop.label || '').toCamelCase(); + } + }; + $scope.validate = function (crop) { + $scope.validateWidth(crop); + $scope.validateHeight(crop); + $scope.validateAlias(crop); + }; + $scope.validateWidth = function (crop) { + crop.hasWidthError = !(Utilities.isNumber(crop.width) && crop.width > 0); + }; + $scope.validateHeight = function (crop) { + crop.hasHeightError = !(Utilities.isNumber(crop.height) && crop.height > 0); + }; + $scope.validateAlias = function (crop, $event) { + var exists = $scope.model.value.find(function (x) { + return crop !== x && crop.alias === x.alias; + }); + if (exists !== undefined || crop.alias === '') { + // alias is not valid + crop.hasAliasError = true; + } else { + // everything was good: + crop.hasAliasError = false; + } + }; + $scope.confirmChanges = function (crop, event) { + if (event.keyCode == 13) { + $scope.setChanges(crop, event); + event.preventDefault(); + } + }; + $scope.focusNextField = function (event) { + if (event.keyCode == 13) { + var el = event.target; + var inputs = Array.from(document.querySelectorAll('input:not(disabled)')); + var inputIndex = inputs.indexOf(el); + if (inputIndex > -1) { + var nextIndex = inputs.indexOf(el) + 1; + if (inputs.length > nextIndex) { + inputs[nextIndex].focus(); + event.preventDefault(); + } + } + } + }; + $scope.sortableOptions = { + axis: 'y', + containment: 'parent', + cursor: 'move', + tolerance: 'pointer' + }; + $scope.$on('$destroy', function () { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + try { + for (var _iterator = unsubscribe[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var subscription = _step.value; + subscription(); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return != null) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + }); + }); + 'use strict'; + (function () { + 'use strict'; + angular.module('umbraco').controller('Umbraco.PropertyEditors.MediaPicker3PropertyEditor.CreateButtonController', function Controller($scope) { + var vm = this; + vm.plusPosY = 0; + vm.onMouseMove = function ($event) { + vm.plusPosY = $event.offsetY; + }; + }); + }()); + 'use strict'; //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, editorService, memberGroupResource) { + var vm = this; + vm.openMemberGroupPicker = openMemberGroupPicker; + vm.remove = remove; + vm.clear = clear; function trim(str, chr) { var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); return str.replace(rgxtrim, ''); @@ -20275,7 +22622,12 @@ $scope.renderModel = groups; }); } - $scope.openMemberGroupPicker = function () { + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + function openMemberGroupPicker() { var memberGroupPicker = { multiPicker: true, submit: function submit(model) { @@ -20287,38 +22639,28 @@ if (newGroupIds && newGroupIds.length) { memberGroupResource.getByIds(newGroupIds).then(function (groups) { $scope.renderModel = _.union($scope.renderModel, groups); + setDirty(); editorService.close(); }); } else { // no new groups selected editorService.close(); } - editorService.close(); }, close: function close() { editorService.close(); } }; editorService.memberGroupPicker(memberGroupPicker); - }; - $scope.remove = function (index) { + } + function remove(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 () { + setDirty(); + } + function clear() { $scope.renderModel = []; - }; + setDirty(); + } function renderModelIds() { return _.map($scope.renderModel, function (i) { return i.id; @@ -20335,6 +22677,9 @@ angular.module('umbraco').controller('Umbraco.PropertyEditors.MemberGroupPickerController', memberGroupPicker); 'use strict'; function memberGroupController($scope, editorService, memberGroupResource) { + var vm = this; + vm.pickGroup = pickGroup; + vm.removeGroup = removeGroup; //set the selected to the keys of the dictionary who's value is true $scope.getSelected = function () { var selected = []; @@ -20345,7 +22690,12 @@ } return selected; }; - $scope.pickGroup = function () { + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + function pickGroup() { editorService.memberGroupPicker({ multiPicker: true, submit: function submit(model) { @@ -20357,22 +22707,24 @@ $scope.model.value[group.name] = true; }); }); + setDirty(); editorService.close(); }, close: function close() { editorService.close(); } }); - }; - $scope.removeGroup = function (group) { + } + function removeGroup(group) { $scope.model.value[group] = false; - }; + setDirty(); + } } angular.module('umbraco').controller('Umbraco.PropertyEditors.MemberGroupController', memberGroupController); 'use strict'; //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, entityResource, iconHelper, angularHelper, editorService) { + function memberPickerController($scope, entityResource, iconHelper, editorService) { function trim(str, chr) { var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); return str.replace(rgxtrim, ''); @@ -20389,7 +22741,7 @@ }, filterCssClass: 'not-allowed', callback: function callback(data) { - if (angular.isArray(data)) { + if (Utilities.isArray(data)) { _.each(data, function (item, i) { $scope.add(item); }); @@ -20397,13 +22749,17 @@ $scope.clear(); $scope.add(data); } - angularHelper.getCurrentForm($scope).$setDirty(); } }; + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$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); + Utilities.extend(dialogOptions, $scope.model.config); } $scope.openMemberPicker = function () { var memberPicker = dialogOptions; @@ -20422,6 +22778,7 @@ }; $scope.remove = function (index) { $scope.renderModel.splice(index, 1); + setDirty(); }; $scope.add = function (item) { var currIds = _.map($scope.renderModel, function (i) { @@ -20440,6 +22797,7 @@ udi: item.udi, icon: item.icon }); + setDirty(); } }; $scope.clear = function () { @@ -20490,7 +22848,7 @@ if (!$scope.model.value) { $scope.model.value = []; } - //add any fields that there isn't values for + // 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) { @@ -20506,7 +22864,7 @@ 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 + // Focus on the newly added value $scope.model.value[newItemIndex].hasFocus = true; } break; @@ -20527,11 +22885,11 @@ } $scope.model.value = remainder; var prevItemIndex = index - 1; - //Set focus back on false as the directive only watches for true + // 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 + // Focus on the previous value $scope.model.value[prevItemIndex].hasFocus = true; }); } @@ -20546,7 +22904,7 @@ $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 + // Focus new value var newItemIndex = $scope.model.value.length - 1; $scope.model.value[newItemIndex].hasFocus = true; } @@ -20582,10 +22940,21 @@ $timeout(function () { validate(); }); + // We always need to ensure we dont submit anything broken + var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { + // Filter to items with values + $scope.model.value = $scope.model.value.filter(function (el) { + return el.value.trim() !== ''; + }) || []; + }); + // When the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); } angular.module('umbraco').controller('Umbraco.PropertyEditors.MultipleTextBoxController', MultipleTextBoxController); 'use strict'; - function multiUrlPickerController($scope, angularHelper, localizationService, entityResource, iconHelper, editorService) { + function multiUrlPickerController($scope, localizationService, entityResource, iconHelper, editorService) { var vm = { labels: { general_recycleBin: '' } }; $scope.renderModel = []; if ($scope.preview) { @@ -20594,7 +22963,6 @@ if (!Array.isArray($scope.model.value)) { $scope.model.value = []; } - var currentForm = angularHelper.getCurrentForm($scope); $scope.sortableOptions = { axis: 'y', containment: 'parent', @@ -20604,7 +22972,7 @@ scroll: true, zIndex: 6000, update: function update() { - currentForm.$setDirty(); + setDirty(); } }; $scope.model.value.forEach(function (link) { @@ -20629,10 +22997,12 @@ $scope.multiUrlPickerForm.maxCount.$setValidity('maxCount', true); } $scope.sortableOptions.disabled = $scope.renderModel.length === 1; + //Update value + $scope.model.value = $scope.renderModel; }); $scope.remove = function ($index) { $scope.renderModel.splice($index, 1); - currentForm.$setDirty(); + setDirty(); }; $scope.openLinkPicker = function (link, $index) { var target = link ? { @@ -20647,6 +23017,7 @@ dataTypeKey: $scope.model.dataTypeKey, ignoreUserStartNodes: $scope.model.config && $scope.model.config.ignoreUserStartNodes ? $scope.model.config.ignoreUserStartNodes : '0', hideAnchor: $scope.model.config && $scope.model.config.hideAnchor ? true : false, + size: $scope.model.config.overlaySize, submit: function submit(model) { if (model.target.url || model.target.anchor) { // if an anchor exists, check that it is appropriately prefixed @@ -20683,7 +23054,7 @@ link.icon = 'icon-link'; link.published = true; } - currentForm.$setDirty(); + setDirty(); } editorService.close(); }, @@ -20693,6 +23064,11 @@ }; editorService.linkPicker(linkPicker); }; + function setDirty() { + if ($scope.multiUrlPickerForm) { + $scope.multiUrlPickerForm.modelValue.$setDirty(); + } + } function init() { localizationService.localizeMany(['general_recycleBin']).then(function (data) { vm.labels.general_recycleBin = data[0]; @@ -20736,41 +23112,41 @@ angular.module('umbraco').run([ 'clipboardService', function (clipboardService) { - function clearNestedContentPropertiesForStorage(prop, propClearingMethod) { + function resolveNestedContentPropertiesForPaste(prop, propClearingMethod) { // if prop.editor is "Umbraco.NestedContent" if (_typeof(prop) === 'object' && prop.editor === 'Umbraco.NestedContent') { var value = prop.value; for (var i = 0; i < value.length; i++) { var obj = value[i]; - // remove the key - delete obj.key; + // generate a new key. + obj.key = String.CreateGuid(); // Loop through all inner properties: for (var k in obj) { - propClearingMethod(obj[k]); + propClearingMethod(obj[k], clipboardService.TYPES.RAW); } } } } - clipboardService.registrerClearPropertyResolver(clearNestedContentPropertiesForStorage); - function clearInnerNestedContentPropertiesForStorage(prop, propClearingMethod) { - // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property inside a NestedContent property. + clipboardService.registerPastePropertyResolver(resolveNestedContentPropertiesForPaste, clipboardService.TYPES.ELEMENT_TYPE); + function resolveInnerNestedContentPropertiesForPaste(prop, propClearingMethod) { + // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property data. if (Array.isArray(prop) && prop.length > 0 && prop[0].ncContentTypeAlias !== undefined) { for (var i = 0; i < prop.length; i++) { var obj = prop[i]; - // remove the key - delete obj.key; + // generate a new key. + obj.key = String.CreateGuid(); // Loop through all inner properties: for (var k in obj) { - propClearingMethod(obj[k]); + propClearingMethod(obj[k], clipboardService.TYPES.RAW); } } } } - clipboardService.registrerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage); + clipboardService.registerPastePropertyResolver(resolveInnerNestedContentPropertiesForPaste, clipboardService.TYPES.RAW); } ]); angular.module('umbraco').component('nestedContentPropertyEditor', { - template: '
Minimum %0% entries, needs %1% more.
Maximum %0% entries, %1% too many.
', + template: '
No content types are configured for this property.
Minimum %0% entries, needs %1% more.
Maximum %0% entries, %1% too many.
', controller: NestedContentController, controllerAs: 'vm', require: { @@ -20778,7 +23154,7 @@ umbVariantContent: '?^^umbVariantContent' } }); - function NestedContentController($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService) { + function NestedContentController($scope, $interpolate, $filter, serverValidationManager, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService) { var vm = this; var model = $scope.$parent.$parent.model; var contentTypeAliases = []; @@ -20802,6 +23178,9 @@ vm.showIcons = Object.toBoolean(model.config.showIcons); vm.wideMode = Object.toBoolean(model.config.hideLabel); vm.hasContentTypes = model.config.contentTypes.length > 0; + var cultureChanged = eventsService.on('editors.content.cultureChanged', function (name, args) { + return updateModel(); + }); var labels = {}; vm.labels = labels; localizationService.localizeMany([ @@ -20813,9 +23192,10 @@ labels.content_createEmpty = data[1]; labels.copy_icon_title = data[2]; }); - function setCurrentNode(node) { + function setCurrentNode(node, focusNode) { updateModel(); vm.currentNode = node; + vm.focusOnNode = focusNode; } var copyAllEntries = function copyAllEntries() { syncCurrentNode(); @@ -20835,7 +23215,7 @@ model.label, nodeName ]).then(function (data) { - clipboardService.copyArray('elementTypeArray', aliases, vm.nodes, data, 'icon-thumbnail-list', model.id, clearNodeForCopy); + clipboardService.copyArray(clipboardService.TYPES.ELEMENT_TYPE, aliases, vm.nodes, data, 'icon-thumbnail-list', model.id, clearNodeForCopy); }); }; var copyAllEntriesAction = { @@ -20874,117 +23254,110 @@ }; // helper to force the current form into the dirty state function setDirty() { - if ($scope.$parent.$parent.propertyForm) { - $scope.$parent.$parent.propertyForm.$setDirty(); + if (vm.umbProperty) { + vm.umbProperty.setDirty(); } } ; function addNode(alias) { var scaffold = getScaffold(alias); var newNode = createNode(scaffold, null); - setCurrentNode(newNode); + setCurrentNode(newNode, true); setDirty(); validate(); } ; vm.openNodeTypePicker = function ($event) { - if (vm.overlayMenu || vm.nodes.length >= vm.maxItems) { + if (vm.nodes.length >= vm.maxItems) { return; } - vm.overlayMenu = { - show: false, - style: {}, - filter: vm.scaffolds.length > 12 ? true : false, + var availableItems = []; + _.each(vm.scaffolds, function (scaffold) { + availableItems.push({ + alias: scaffold.contentTypeAlias, + name: scaffold.contentTypeName, + icon: iconHelper.convertFromLegacyIcon(scaffold.icon), + tooltip: scaffold.documentType.description + }); + }); + var dialog = { orderBy: '$index', view: 'itempicker', event: $event, + filter: availableItems.length > 12, + size: availableItems.length > 6 ? 'medium' : 'small', + availableItems: availableItems, clickPasteItem: function clickPasteItem(item) { - if (item.type === 'elementTypeArray') { + if (Array.isArray(item.data)) { _.each(item.data, function (entry) { pasteFromClipboard(entry); }); } else { pasteFromClipboard(item.data); } - vm.overlayMenu.show = false; - vm.overlayMenu = null; + overlayService.close(); }, submit: function submit(model) { if (model && model.selectedItem) { addNode(model.selectedItem.alias); } - vm.overlayMenu.show = false; - vm.overlayMenu = null; + overlayService.close(); }, close: function close() { - vm.overlayMenu.show = false; - vm.overlayMenu = null; + overlayService.close(); } }; - // this could be used for future limiting on node types - vm.overlayMenu.availableItems = []; - _.each(vm.scaffolds, function (scaffold) { - vm.overlayMenu.availableItems.push({ - alias: scaffold.contentTypeAlias, - name: scaffold.contentTypeName, - icon: iconHelper.convertFromLegacyIcon(scaffold.icon) - }); - }); - if (vm.overlayMenu.availableItems.length === 0) { + if (dialog.availableItems.length === 0) { return; } - vm.overlayMenu.size = vm.overlayMenu.availableItems.length > 6 ? 'medium' : 'small'; - vm.overlayMenu.pasteItems = []; - var singleEntriesForPaste = clipboardService.retriveEntriesOfType('elementType', contentTypeAliases); - _.each(singleEntriesForPaste, function (entry) { - vm.overlayMenu.pasteItems.push({ - type: 'elementType', + dialog.pasteItems = []; + var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); + _.each(entriesForPaste, function (entry) { + dialog.pasteItems.push({ + date: entry.date, name: entry.label, data: entry.data, icon: entry.icon }); }); - var arrayEntriesForPaste = clipboardService.retriveEntriesOfType('elementTypeArray', contentTypeAliases); - _.each(arrayEntriesForPaste, function (entry) { - vm.overlayMenu.pasteItems.push({ - type: 'elementTypeArray', - name: entry.label, - data: entry.data, - icon: entry.icon - }); + dialog.pasteItems.sort(function (a, b) { + return b.date - a.date; }); - vm.overlayMenu.title = labels.grid_addElement; - vm.overlayMenu.hideHeader = vm.overlayMenu.pasteItems.length > 0; - vm.overlayMenu.clickClearPaste = function ($event) { + dialog.title = dialog.pasteItems.length > 0 ? labels.grid_addElement : labels.content_createEmpty; + dialog.hideHeader = dialog.pasteItems.length > 0; + dialog.clickClearPaste = function ($event) { $event.stopPropagation(); $event.preventDefault(); - clipboardService.clearEntriesOfType('elementType', contentTypeAliases); - clipboardService.clearEntriesOfType('elementTypeArray', contentTypeAliases); - vm.overlayMenu.pasteItems = []; + clipboardService.clearEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); + dialog.pasteItems = []; // This dialog is not connected via the clipboardService events, so we need to update manually. - vm.overlayMenu.hideHeader = false; + dialog.hideHeader = false; }; - if (vm.overlayMenu.availableItems.length === 1 && vm.overlayMenu.pasteItems.length === 0) { + if (dialog.availableItems.length === 1 && dialog.pasteItems.length === 0) { // only one scaffold type - no need to display the picker addNode(vm.scaffolds[0].contentTypeAlias); - vm.overlayMenu = null; + dialog.close(); return; } - vm.overlayMenu.show = true; + overlayService.open(dialog); }; vm.editNode = function (idx) { if (vm.currentNode && vm.currentNode.key === vm.nodes[idx].key) { - setCurrentNode(null); + setCurrentNode(null, false); } else { - setCurrentNode(vm.nodes[idx]); + setCurrentNode(vm.nodes[idx], true); } }; vm.canDeleteNode = function (idx) { return vm.nodes.length > vm.minItems ? true : model.config.contentTypes.length > 1; }; function deleteNode(idx) { - vm.nodes.splice(idx, 1); + var removed = vm.nodes.splice(idx, 1); setDirty(); + removed.forEach(function (x) { + // remove any server validation errors associated + serverValidationManager.removePropertyError(x.key, vm.umbProperty.property.culture, vm.umbProperty.property.segment, '', { matchType: 'contains' }); + }); updateModel(); validate(); } @@ -21035,7 +23408,7 @@ // Add a temporary index property item['$index'] = idx + 1; var newName = contentType.nameExp(item); - if (newName && (newName = $.trim(newName))) { + if (newName && (newName = newName.trim())) { name = newName; } // Delete the index property as we don't want to persist it @@ -21068,6 +23441,7 @@ }; vm.sortableOptions = { axis: 'y', + containment: 'parent', cursor: 'move', handle: '.umb-nested-content__header-bar', distance: 10, @@ -21112,50 +23486,81 @@ function clearNodeForCopy(clonedData) { delete clonedData.key; delete clonedData.$$hashKey; + var variant = clonedData.variants[0]; + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + // If we have ncSpecific data, lets revert to standard data model. + if (prop.propertyAlias) { + prop.alias = prop.propertyAlias; + delete prop.propertyAlias; + } + if (prop.ncMandatory !== undefined) { + prop.validation.mandatory = prop.ncMandatory; + delete prop.ncMandatory; + } + } + } } vm.showCopy = clipboardService.isSupported(); vm.showPaste = false; vm.clickCopy = function ($event, node) { syncCurrentNode(); - clipboardService.copy('elementType', node.contentTypeAlias, node, null, null, null, clearNodeForCopy); + clipboardService.copy(clipboardService.TYPES.ELEMENT_TYPE, node.contentTypeAlias, node, null, null, null, clearNodeForCopy); $event.stopPropagation(); }; function pasteFromClipboard(newNode) { if (newNode === undefined) { return; } + newNode = clipboardService.parseContentForPaste(newNode, clipboardService.TYPES.ELEMENT_TYPE); // generate a new key. newNode.key = String.CreateGuid(); + // Ensure we have NC data in place: + var variant = newNode.variants[0]; + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + extendPropertyWithNCData(tab.properties[p]); + } + } vm.nodes.push(newNode); setDirty(); //updateModel();// done by setting current node... - setCurrentNode(newNode); + setCurrentNode(newNode, true); } function checkAbilityToPasteContent() { - vm.showPaste = clipboardService.hasEntriesOfType('elementType', contentTypeAliases) || clipboardService.hasEntriesOfType('elementTypeArray', contentTypeAliases); + vm.showPaste = clipboardService.hasEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); } - eventsService.on('clipboardService.storageUpdate', checkAbilityToPasteContent); + var storageUpdate = eventsService.on('clipboardService.storageUpdate', checkAbilityToPasteContent); + $scope.$on('$destroy', function () { + storageUpdate(); + }); var notSupported = [ 'Umbraco.Tags', 'Umbraco.UploadField', - 'Umbraco.ImageCropper' + 'Umbraco.ImageCropper', + 'Umbraco.BlockList' ]; // Initialize - var scaffoldsLoaded = 0; vm.scaffolds = []; - _.each(model.config.contentTypes, function (contentType) { - contentResource.getScaffold(-20, contentType.ncAlias).then(function (scaffold) { + contentResource.getScaffolds(-20, contentTypeAliases).then(function (scaffolds) { + // Loop through all the content types + _.each(model.config.contentTypes, function (contentType) { + // Get the scaffold from the result + var scaffold = scaffolds[contentType.ncAlias]; // make sure it's an element type before allowing the user to create new ones if (scaffold.isElement) { // remove all tabs except the specified tab var tabs = scaffold.variants[0].tabs; var tab = _.find(tabs, function (tab) { - return tab.id !== 0 && (tab.alias.toLowerCase() === contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias === ''); + return tab.id !== 0 && (tab.label.toLowerCase() === contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias === ''); }); scaffold.variants[0].tabs = []; if (tab) { scaffold.variants[0].tabs.push(tab); - angular.forEach(tab.properties, function (property) { + tab.properties.forEach(function (property) { if (_.find(notSupported, function (x) { return x === property.editor; })) { @@ -21165,79 +23570,103 @@ } }); } + // Ensure Culture Data for Complex Validation. + ensureCultureData(scaffold); // Store the scaffold object vm.scaffolds.push(scaffold); } - scaffoldsLoaded++; - initIfAllScaffoldsHaveLoaded(); - }, function (error) { - scaffoldsLoaded++; - initIfAllScaffoldsHaveLoaded(); }); + // Initialize once all scaffolds have been loaded + initNestedContent(); }); - var initIfAllScaffoldsHaveLoaded = function initIfAllScaffoldsHaveLoaded() { + /** + * Ensure that the containing content variant language and current property culture is transferred along + * to the scaffolded content object representing this block. + * This is required for validation along with ensuring that the umb-property inheritance is constantly maintained. + * @param {any} content + */ + function ensureCultureData(content) { + if (!content || !vm.umbVariantContent || !vm.umbProperty) + return; + if (vm.umbVariantContent.editor.content.language) { + // set the scaffolded content's language to the language of the current editor + content.language = vm.umbVariantContent.editor.content.language; + } + // currently we only ever deal with invariant content for blocks so there's only one + content.variants[0].tabs.forEach(function (tab) { + tab.properties.forEach(function (prop) { + // set the scaffolded property to the culture of the containing property + prop.culture = vm.umbProperty.property.culture; + }); + }); + } + var initNestedContent = function initNestedContent() { // Initialize when all scaffolds have loaded - if (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. - contentTypeAliases = []; - _.each(model.config.contentTypes, function (contentType) { - contentTypeAliases.push(contentType.ncAlias); - }); - vm.scaffolds = $filter('orderBy')(vm.scaffolds, function (s) { - return contentTypeAliases.indexOf(s.contentTypeAlias); - }); - // Convert stored nodes - if (model.value) { - for (var i = 0; i < model.value.length; i++) { - var item = model.value[i]; - var scaffold = getScaffold(item.ncContentTypeAlias); - if (scaffold == null) { - // No such scaffold - the content type might have been deleted. We need to skip it. - continue; - } - createNode(scaffold, item); - } - } - // Enforce min items if we only have one scaffold type - var modelWasChanged = false; - if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { - for (var i = vm.nodes.length; i < model.config.minItems; i++) { - addNode(vm.scaffolds[0].contentTypeAlias); + // Sort the scaffold explicitly according to the sort order defined by the data type. + vm.scaffolds = $filter('orderBy')(vm.scaffolds, function (s) { + return contentTypeAliases.indexOf(s.contentTypeAlias); + }); + // Convert stored nodes + if (model.value) { + for (var i = 0; i < model.value.length; i++) { + var item = model.value[i]; + var scaffold = getScaffold(item.ncContentTypeAlias); + if (scaffold == null) { + // No such scaffold - the content type might have been deleted. We need to skip it. + continue; } - modelWasChanged = true; - } - // If there is only one item, set it as current node - if (vm.singleMode || vm.nodes.length === 1 && vm.maxItems === 1) { - setCurrentNode(vm.nodes[0]); + createNode(scaffold, item); } - validate(); - vm.inited = true; - if (modelWasChanged) { - updateModel(); + } + // Enforce min items if we only have one scaffold type + var modelWasChanged = false; + if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { + for (var i = vm.nodes.length; i < model.config.minItems; i++) { + addNode(vm.scaffolds[0].contentTypeAlias); } - updatePropertyActionStates(); - checkAbilityToPasteContent(); + modelWasChanged = true; + } + // If there is only one item, set it as current node + if (vm.singleMode || vm.nodes.length === 1 && vm.maxItems === 1) { + setCurrentNode(vm.nodes[0], false); } + validate(); + vm.inited = true; + if (modelWasChanged) { + updateModel(); + } + updatePropertyActionStates(); + checkAbilityToPasteContent(); }; + function extendPropertyWithNCData(prop) { + if (prop.propertyAlias === undefined) { + // store the original alias before we change below, see notes + prop.propertyAlias = prop.alias; + // NOTE: This is super ugly, the reason it is like this is because it controls the label/html id in the umb-property component at a higher level. + // not pretty :/ but we can't change this now since it would require a bunch of plumbing to be able to change the id's higher up. + prop.alias = model.alias + '___' + prop.alias; + } + // TODO: Do we need to deal with this separately? + // Force validation to occur server side as this is the + // only way we can have consistency between mandatory and + // regex validation messages. Not ideal, but it works. + if (prop.ncMandatory === undefined) { + prop.ncMandatory = prop.validation.mandatory; + prop.validation = { + mandatory: false, + pattern: '' + }; + } + } function createNode(scaffold, fromNcEntry) { - var node = angular.copy(scaffold); + var node = Utilities.copy(scaffold); node.key = fromNcEntry && fromNcEntry.key ? fromNcEntry.key : String.CreateGuid(); var variant = node.variants[0]; for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; - prop.propertyAlias = prop.alias; - prop.alias = model.alias + '___' + prop.alias; - // Force validation to occur server side as this is the - // only way we can have consistency between mandatory and - // regex validation messages. Not ideal, but it works. - prop.ncMandatory = prop.validation.mandatory; - prop.validation = { - mandatory: false, - pattern: '' - }; + extendPropertyWithNCData(prop); if (fromNcEntry && fromNcEntry[prop.propertyAlias]) { prop.value = fromNcEntry[prop.propertyAlias]; } @@ -21288,8 +23717,8 @@ removeAllEntriesAction ]; this.$onInit = function () { - if (this.umbProperty) { - this.umbProperty.setPropertyActions(propertyActions); + if (vm.umbProperty) { + vm.umbProperty.setPropertyActions(propertyActions); } }; var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { @@ -21314,6 +23743,7 @@ }); $scope.$on('$destroy', function () { unsubscribe(); + cultureChanged(); watcher(); }); } @@ -21450,8 +23880,9 @@ var vm = this; vm.viewItems = []; function init() { + vm.uniqueId = String.CreateGuid(); //we can't really do anything if the config isn't an object - if (angular.isObject($scope.model.config.items)) { + if (Utilities.isObject($scope.model.config.items)) { // formatting the items in the dictionary into an array var sortedItems = []; var vals = _.values($scope.model.config.items); @@ -21580,7 +24011,7 @@ $scope.model.value.splice(idx, 1); }; $scope.add = function ($event) { - if (!angular.isArray($scope.model.value)) { + if (!Utilities.isArray($scope.model.value)) { $scope.model.value = []; } if ($scope.newCaption == '') { @@ -21778,7 +24209,7 @@ // we have this mini content editor panel that can be launched with MNTP. $scope.textAreaHtmlId = $scope.model.alias + '_' + String.CreateGuid(); var editorConfig = $scope.model.config ? $scope.model.config.editor : null; - if (!editorConfig || angular.isString(editorConfig)) { + if (!editorConfig || Utilities.isString(editorConfig)) { editorConfig = tinyMceService.defaultPrevalues(); } //make sure there's a max image size @@ -21832,10 +24263,10 @@ tinyMceService.initializeEditor({ editor: editor, model: $scope.model, - currentForm: angularHelper.getCurrentForm($scope) + currentFormInput: $scope.rteForm.modelValue }); }; - angular.extend(baseLineConfigObj, standardConfig); + Utilities.extend(baseLineConfigObj, standardConfig); // We need to wait for DOM to have rendered before we can find the element by ID. $timeout(function () { tinymce.init(baseLineConfigObj); @@ -21864,7 +24295,7 @@ 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)) { + if (Utilities.isString($scope.model.value)) { $scope.model.value = cfg; } } else { @@ -21887,16 +24318,24 @@ // 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.alias); - return angular.extend(obj, { + var objCmd = Utilities.extend(obj, { fontIcon: icon.name, isCustom: icon.isCustom, - selected: $scope.model.value.toolbar.indexOf(obj.alias) >= 0 + selected: $scope.model.value.toolbar.indexOf(obj.alias) >= 0, + icon: 'mce-ico ' + (icon.isCustom ? ' mce-i-custom ' : ' mce-i-') + icon.name }); + return objCmd; }); }); stylesheetResource.getAll().then(function (stylesheets) { $scope.stylesheets = stylesheets; - _.each($scope.stylesheets, function (stylesheet) { + // if the CSS directory changes, previously assigned stylesheets are retained, but will not be visible + // and will throw a 404 when loading the RTE. Remove them here. Still needs to be saved... + var cssPath = Umbraco.Sys.ServerVariables.umbracoSettings.cssPath; + $scope.model.value.stylesheets = $scope.model.value.stylesheets.filter(function (sheet) { + return sheet.startsWith(cssPath); + }); + $scope.stylesheets.forEach(function (stylesheet) { // support both current format (full stylesheet path) and legacy format (stylesheet name only) stylesheet.selected = $scope.model.value.stylesheets.indexOf(stylesheet.path) >= 0 || $scope.model.value.stylesheets.indexOf(stylesheet.name) >= 0; }); @@ -21972,7 +24411,7 @@ assetsService.loadCss('lib/tinymce/skins/lightgray/skin.min.css', $scope); }); 'use strict'; - function sliderController($scope, angularHelper) { + function sliderController($scope) { var sliderRef = null; /** configure some defaults on init */ function configureDefaults() { @@ -21985,7 +24424,12 @@ } function setModelValue(values) { $scope.model.value = values ? values.toString() : null; - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); + } + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } } $scope.setup = function (slider) { sliderRef = slider; @@ -22025,7 +24469,7 @@ return value.toFixed(stepDecimalPlaces); }, from: function from(value) { - return value; + return Number(value); } }, 'range': { @@ -22061,37 +24505,34 @@ 'use strict'; function textAreaController($scope, validationMessageService) { // macro parameter editor doesn't contains a config object, - // so we create a new one to hold any properties + // so we create a new one to hold any properties if (!$scope.model.config) { $scope.model.config = {}; } - $scope.model.count = 0; - 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; - } + $scope.maxChars = $scope.model.config.maxChars || 0; + $scope.maxCharsLimit = $scope.model.config && $scope.model.config.maxChars > 0; + $scope.charsCount = 0; + $scope.nearMaxLimit = false; + $scope.validLength = true; $scope.$on('formSubmitting', function () { - if ($scope.isLengthValid()) { + if ($scope.validLength) { $scope.textareaFieldForm.textarea.$setValidity('maxChars', true); } else { $scope.textareaFieldForm.textarea.$setValidity('maxChars', false); } }); - $scope.isLengthValid = function () { - if (!$scope.model.maxlength) { - return true; - } - return $scope.model.config.maxChars >= $scope.model.count; - }; - $scope.model.change = function () { + function checkLengthVadility() { + $scope.validLength = !($scope.maxCharsLimit === true && $scope.charsCount > $scope.maxChars); + } + $scope.change = function () { if ($scope.model.value) { - $scope.model.count = $scope.model.value.length; + $scope.charsCount = $scope.model.value.length; + checkLengthVadility(); + $scope.nearMaxLimit = $scope.maxCharsLimit === true && $scope.validLength === true && $scope.charsCount > Math.max($scope.maxChars * 0.8, $scope.maxChars - 50); } }; - $scope.model.change(); + $scope.model.onValueChanged = $scope.change; + $scope.change(); // Set the message to use for when a mandatory field isn't completed. // Will either use the one provided on the property type or a localised default. validationMessageService.getMandatoryMessage($scope.model.validation).then(function (value) { @@ -22106,29 +24547,35 @@ if (!$scope.model.config) { $scope.model.config = {}; } - $scope.model.count = 0; - 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; - } + // 512 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.maxChars = Math.min($scope.model.config.maxChars || 512, 512); + $scope.charsCount = 0; + $scope.nearMaxLimit = false; + $scope.validLength = true; $scope.$on('formSubmitting', function () { - if ($scope.isLengthValid()) { + if ($scope.validLength === true) { $scope.textboxFieldForm.textbox.$setValidity('maxChars', true); } else { $scope.textboxFieldForm.textbox.$setValidity('maxChars', false); } }); - $scope.isLengthValid = function () { - return $scope.model.config.maxChars >= $scope.model.count; - }; - $scope.model.change = function () { + function checkLengthVadility() { + $scope.validLength = $scope.charsCount <= $scope.maxChars; + } + $scope.change = function () { if ($scope.model.value) { - $scope.model.count = $scope.model.value.length; + $scope.charsCount = $scope.model.value.length; + checkLengthVadility(); + $scope.nearMaxLimit = $scope.validLength && $scope.charsCount > Math.max($scope.maxChars * 0.8, $scope.maxChars - 25); + } else { + $scope.charsCount = 0; + checkLengthVadility(); } }; - $scope.model.change(); + $scope.model.onValueChanged = $scope.change; + $scope.change(); // Set the message to use for when a mandatory field isn't completed. // Will either use the one provided on the property type or a localised default. validationMessageService.getMandatoryMessage($scope.model.validation).then(function (value) { @@ -22139,7 +24586,7 @@ 'use strict'; angular.module('umbraco').controller('Umbraco.PropertyEditors.UrlListController', function ($rootScope, $scope, $filter) { function formatDisplayValue() { - if (angular.isArray($scope.model.value)) { + if (Utilities.isArray($scope.model.value)) { //it's the json value $scope.renderModel = _.map($scope.model.value, function (item) { return { @@ -22176,6 +24623,118 @@ }; }); 'use strict'; + function userPickerController($scope, iconHelper, editorService, overlayService, entityResource) { + 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 multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + $scope.openUserPicker = function () { + var currentSelection = []; + var userPicker = { + multiPicker: multiPicker, + selection: currentSelection, + submit: function submit(model) { + if (model.selection) { + _.each(model.selection, function (item, i) { + $scope.add(item); + }); + } + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.userPicker(userPicker); + }; + $scope.remove = function (index) { + var dialog = { + view: 'views/propertyeditors/userpicker/overlays/remove.html', + username: $scope.renderModel[index].name, + submitButtonLabelKey: 'defaultdialogs_yesRemove', + submitButtonStyle: 'danger', + submit: function submit() { + $scope.renderModel.splice(index, 1); + $scope.userName = ''; + setDirty(); + overlayService.close(); + }, + close: function close() { + overlayService.close(); + } + }; + overlayService.open(dialog); + }; + $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 = item.icon ? iconHelper.convertFromLegacyIcon(item.icon) : 'icon-user'; + $scope.renderModel.push({ + name: item.name, + id: item.id, + udi: item.udi, + icon: item.icon, + avatars: item.avatars + }); + setDirty(); + } + }; + $scope.clear = function () { + $scope.renderModel = []; + setDirty(); + }; + 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 user data - split to an array of ints (map) + var modelIds = $scope.model.value ? $scope.model.value.split(',').map(function (x) { + return +x; + }) : []; + if (modelIds.length !== 0) { + entityResource.getAll('User').then(function (users) { + var filteredUsers = users.filter(function (user) { + return modelIds.indexOf(user.id) !== -1; + }); + filteredUsers.forEach(function (item) { + $scope.renderModel.push({ + name: item.name, + id: item.id, + udi: item.udi, + icon: item.icon = item.icon ? iconHelper.convertFromLegacyIcon(item.icon) : 'icon-user', + avatars: item.avatars + }); + }); + }); + } + } + angular.module('umbraco').controller('Umbraco.PropertyEditors.UserPickerController', userPickerController); + 'use strict'; /** * @ngdoc controller * @name Umbraco.Editors.RelationTypes.CreateController @@ -22201,7 +24760,7 @@ function createRelationType() { if (formHelper.submitForm({ scope: $scope, - formCtrl: this.createRelationTypeForm, + formCtrl: $scope.createRelationTypeForm, statusMessage: 'Creating relation type...' })) { var node = $scope.currentNode; @@ -22215,10 +24774,18 @@ forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createRelationTypeForm + }); var currentSection = appState.getSectionState('currentSection'); $location.path('/' + currentSection + '/relationTypes/edit/' + data); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: $scope.createRelationTypeForm, + hasErrors: true + }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); @@ -22309,11 +24876,14 @@ ]; }); // load references when the 'relations' tab is first activated/switched to - eventsService.on('app.tabChange', function (event, args) { + var appTabChange = eventsService.on('app.tabChange', function (event, args) { if (args.alias === 'relations') { loadRelations(); } }); + $scope.$on('$destroy', function () { + appTabChange(); + }); // Inital page/overview API call of relation type relationTypeResource.getById($routeParams.id).then(function (data) { bindRelationType(data); @@ -22349,7 +24919,7 @@ function formatDates(relations) { if (relations) { userService.getCurrentUser().then(function (currentUser) { - angular.forEach(relations, function (relation) { + relations.forEach(function (relation) { relation.timestampFormatted = dateHelper.getLocalDate(relation.createDate, currentUser.locale, 'LLL'); }); }); @@ -22362,13 +24932,14 @@ })) { vm.page.saveButtonState = 'busy'; relationTypeResource.save(vm.relationType).then(function (data) { - formHelper.resetForm({ - scope: $scope, - notifications: data.notifications - }); + formHelper.resetForm({ scope: $scope }); bindRelationType(data); vm.page.saveButtonState = 'success'; }, function (error) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); contentEditingHelper.handleSaveError({ err: error }); notificationsService.error(error.data.message); vm.page.saveButtonState = 'error'; @@ -22436,9 +25007,17 @@ forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: form + }); var section = appState.getSectionState('currentSection'); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: form, + hasErrors: true + }); vm.createFolderError = err; }); } @@ -22679,8 +25258,16 @@ forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: form + }); }, function (err) { + formHelper.resetForm({ + scope: $scope, + formCtrl: form, + hasErrors: true + }); vm.createFolderError = err; }); } @@ -23255,7 +25842,7 @@ // if this is a new template, bind to the blur event on the name if (create) { $timeout(function () { - var nameField = angular.element(document.querySelector('[data-element="editor-name-field"]')); + var nameField = $('[data-element="editor-name-field"]'); if (nameField) { nameField.on('blur', function (event) { if (event.target.value) { @@ -23277,7 +25864,7 @@ }); } // save state of master template to use for comparison when syncing the tree on save - oldMasterTemplateAlias = angular.copy(template.masterTemplateAlias); + oldMasterTemplateAlias = Utilities.copy(template.masterTemplateAlias); // ace configuration vm.aceOption = { mode: 'razor', @@ -23583,7 +26170,7 @@ // make collection of available master templates var availableMasterTemplates = []; // filter out the current template and the selected master template - angular.forEach(vm.templates, function (template) { + vm.templates.forEach(function (template) { if (template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { var templatePathArray = template.path.split(','); // filter descendant templates of current template @@ -23630,7 +26217,7 @@ function getMasterTemplateName(masterTemplateAlias, templates) { if (masterTemplateAlias) { var templateName = ''; - angular.forEach(templates, function (template) { + templates.forEach(function (template) { if (template.alias === masterTemplateAlias) { templateName = template.name; } @@ -23721,20 +26308,26 @@ 'use strict'; (function () { 'use strict'; - function UserGroupEditController($scope, $location, $routeParams, userGroupsResource, localizationService, contentEditingHelper, editorService) { + function UserGroupEditController($scope, $location, $routeParams, userGroupsResource, localizationService, contentEditingHelper, editorService, overlayService) { + var infiniteMode = $scope.model && $scope.model.infiniteMode; + var id = infiniteMode ? $scope.model.id : $routeParams.id; + var create = infiniteMode ? $scope.model.create : $routeParams.create; var vm = this; var contentPickerOpen = false; vm.page = {}; vm.page.rootIcon = 'icon-folder'; + vm.page.submitButtonLabelKey = infiniteMode ? 'buttons_saveAndClose' : 'buttons_save'; vm.userGroup = {}; vm.labels = {}; - vm.showBackButton = true; + vm.showBackButton = !infiniteMode; vm.goToPage = goToPage; vm.openSectionPicker = openSectionPicker; vm.openContentPicker = openContentPicker; vm.openMediaPicker = openMediaPicker; vm.openUserPicker = openUserPicker; - vm.removeSelectedItem = removeSelectedItem; + vm.removeSection = removeSection; + vm.removeAssignedPermissions = removeAssignedPermissions; + vm.removeUser = removeUser; vm.clearStartNode = clearStartNode; vm.save = save; vm.openGranularPermissionsPicker = openGranularPermissionsPicker; @@ -23765,7 +26358,7 @@ localizationService.localize('user_noStartNode').then(function (name) { vm.labels.noStartNode = name; }); - if ($routeParams.create) { + if (create) { // get user group scaffold userGroupsResource.getUserGroupScaffold().then(function (userGroup) { vm.userGroup = userGroup; @@ -23775,7 +26368,7 @@ }); } else { // get user group - userGroupsResource.getUserGroup($routeParams.id).then(function (userGroup) { + userGroupsResource.getUserGroup(id).then(function (userGroup) { vm.userGroup = userGroup; formatGranularPermissionSelection(); setSectionIcon(vm.userGroup.sections); @@ -23794,10 +26387,14 @@ } }).then(function (saved) { vm.userGroup = saved; - formatGranularPermissionSelection(); - setSectionIcon(vm.userGroup.sections); - makeBreadcrumbs(); - vm.page.saveButtonState = 'success'; + if (infiniteMode) { + $scope.model.submit(vm.userGroup); + } else { + formatGranularPermissionSelection(); + setSectionIcon(vm.userGroup.sections); + makeBreadcrumbs(); + vm.page.saveButtonState = 'success'; + } }, function (err) { vm.page.saveButtonState = 'error'; }); @@ -23807,7 +26404,7 @@ } function openSectionPicker() { var currentSelection = []; - angular.copy(vm.userGroup.sections, currentSelection); + Utilities.copy(vm.userGroup.sections, currentSelection); var sectionPicker = { selection: currentSelection, submit: function submit(model) { @@ -23869,7 +26466,7 @@ } function openUserPicker() { var currentSelection = []; - angular.copy(vm.userGroup.users, currentSelection); + Utilities.copy(vm.userGroup.users, currentSelection); var userPicker = { selection: currentSelection, submit: function submit(model) { @@ -23887,15 +26484,15 @@ * 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) { + vm.userGroup.assignedPermissions.forEach(function (node) { formatGranularPermissionSelectionForNode(node); }); } 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) { + Object.values(node.permissions).forEach(function (permissions) { + permissions.forEach(function (p) { if (p.checked) { node.allowedPermissions.push(p); } @@ -23929,7 +26526,7 @@ function setPermissionsForNode(node) { //clone the current defaults to pass to the model if (!node.permissions) { - node.permissions = angular.copy(vm.userGroup.defaultPermissions); + node.permissions = Utilities.copy(vm.userGroup.defaultPermissions); } vm.nodePermissions = { node: node, @@ -23959,10 +26556,28 @@ }; editorService.nodePermissions(vm.nodePermissions); } - function removeSelectedItem(index, selection) { - if (selection && selection.length > 0) { - selection.splice(index, 1); - } + function removeSection(index) { + vm.userGroup.sections.splice(index, 1); + } + function removeAssignedPermissions(index) { + vm.userGroup.assignedPermissions.splice(index, 1); + } + function removeUser(index) { + var dialog = { + view: 'views/users/views/overlays/remove.html', + username: vm.userGroup.users[index].username, + userGroupName: vm.userGroup.name.toLowerCase(), + submitButtonLabelKey: 'defaultdialogs_yesRemove', + submitButtonStyle: 'danger', + submit: function submit() { + vm.userGroup.users.splice(index, 1); + overlayService.close(); + }, + close: function close() { + overlayService.close(); + } + }; + overlayService.open(dialog); } function clearStartNode(type) { if (type === 'content') { @@ -23981,7 +26596,7 @@ ]; } function setSectionIcon(sections) { - angular.forEach(sections, function (section) { + sections.forEach(function (section) { section.icon = 'icon-section'; }); } @@ -24045,7 +26660,8 @@ 'use strict'; (function () { 'use strict'; - function UserEditController($scope, eventsService, $q, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper, editorService, overlayService) { + function UserEditController($scope, eventsService, $q, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper, editorService, overlayService, externalLoginInfoService) { + var currentLoggedInUser = null; var vm = this; vm.page = {}; vm.page.rootIcon = 'icon-folder'; @@ -24060,12 +26676,14 @@ //create the initial model for change password vm.changePasswordModel = { config: {}, - isChanging: false + isChanging: false, + value: {} }; vm.goToPage = goToPage; vm.openUserGroupPicker = openUserGroupPicker; vm.openContentPicker = openContentPicker; vm.openMediaPicker = openMediaPicker; + vm.editSelectedItem = editSelectedItem; vm.removeSelectedItem = removeSelectedItem; vm.disableUser = disableUser; vm.enableUser = enableUser; @@ -24075,7 +26693,10 @@ vm.changeAvatar = changeAvatar; vm.clearAvatar = clearAvatar; vm.save = save; + vm.allowGroupEdit = allowGroupEdit; + vm.changePassword = changePassword; vm.toggleChangePassword = toggleChangePassword; + vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); function init() { vm.loading = true; var labelKeys = [ @@ -24125,6 +26746,7 @@ if (!vm.user.isCurrentUser) { vm.changePasswordModel.config.allowManuallyChangingPassword = true; } + $scope.$emit('$setAccessibleHeader', false, 'general_user', false, vm.user.name, '', true); vm.loading = false; }); }); @@ -24144,19 +26766,35 @@ } } function toggleChangePassword() { - vm.changePasswordModel.isChanging = !vm.changePasswordModel.isChanging; //reset it vm.user.changePassword = null; + localizationService.localizeMany([ + 'general_cancel', + 'general_confirm', + 'general_changePassword' + ]).then(function (data) { + var overlay = { + view: 'changepassword', + title: data[2], + changePassword: vm.user.changePassword, + config: vm.changePasswordModel.config, + closeButtonLabel: data[0], + submitButtonLabel: data[1], + submitButtonStyle: 'success', + close: function close() { + return overlayService.close(); + }, + submit: function submit(model) { + overlayService.close(); + vm.changePasswordModel.value = model.changePassword; + changePassword(); + } + }; + overlayService.open(overlay); + }); } function save() { if (formHelper.submitForm({ scope: $scope })) { - //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) { - //NOTE: the check for allowManuallyChangingPassword is due to this legacy user membership provider setting, if that is true, then the current user - //can change their own password without entering their current one (this is a legacy setting since that is a security issue but we need to maintain compat). - //if allowManuallyChangingPassword=false, then we are using default settings and the user will need to enter their old password to change their own password. - vm.user.changePassword.reset = !vm.user.changePassword.oldPassword && !vm.user.isCurrentUser || vm.changePasswordModel.config.allowManuallyChangingPassword; - } vm.page.saveButtonState = 'busy'; vm.user.resetPasswordValue = null; //save current nav to be restored later so that the tabs dont change @@ -24166,17 +26804,23 @@ extendedSave(saved).then(function (result) { //if all is good, then reset the form formHelper.resetForm({ scope: $scope }); - }, angular.noop); + }, function () { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); + }); vm.user = _.omit(saved, 'navigation'); //restore vm.user.navigation = currentNav; setUserDisplayState(); formatDatesToLocal(vm.user); - vm.changePasswordModel.isChanging = false; - //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.page.saveButtonState = 'success'; }, function (err) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); contentEditingHelper.handleSaveError({ err: err, showNotifications: true @@ -24186,6 +26830,31 @@ } } /** + * + */ + function changePassword() { + //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.changePasswordModel.value) { + //NOTE: the check for allowManuallyChangingPassword is due to this legacy user membership provider setting, if that is true, then the current user + //can change their own password without entering their current one (this is a legacy setting since that is a security issue but we need to maintain compat). + //if allowManuallyChangingPassword=false, then we are using default settings and the user will need to enter their old password to change their own password. + vm.changePasswordModel.value.reset = !vm.changePasswordModel.value.oldPassword && !vm.user.isCurrentUser || vm.changePasswordModel.config.allowManuallyChangingPassword; + } + // since we don't send the entire user model, the id is required + vm.changePasswordModel.value.id = vm.user.id; + usersResource.changePassword(vm.changePasswordModel.value).then(function () { + vm.changePasswordModel.isChanging = false; + vm.changePasswordModel.value = {}; + //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) { + contentEditingHelper.handleSaveError({ + err: err, + showNotifications: true + }); + }); + } + /** * Used to emit the save event and await any async operations being performed by editor extensions * @param {any} savedUser */ @@ -24211,7 +26880,7 @@ } function openUserGroupPicker() { var currentSelection = []; - angular.copy(vm.user.userGroups, currentSelection); + Utilities.copy(vm.user.userGroups, currentSelection); var userGroupPicker = { selection: currentSelection, submit: function submit(model) { @@ -24238,7 +26907,7 @@ submit: function submit(model) { // select items if (model.selection) { - angular.forEach(model.selection, function (item) { + model.selection.forEach(function (item) { if (item.id === '-1') { item.name = vm.labels.contentRoot; item.icon = 'icon-folder'; @@ -24266,7 +26935,7 @@ submit: function submit(model) { // select items if (model.selection) { - angular.forEach(model.selection, function (item) { + model.selection.forEach(function (item) { if (item.id === '-1') { item.name = vm.labels.mediaRoot; item.icon = 'icon-folder'; @@ -24288,7 +26957,7 @@ var found = false; // check if item is already in the selected list if (selection.length > 0) { - angular.forEach(selection, function (selectedItem) { + selection.forEach(function (selectedItem) { if (selectedItem.udi === item.udi) { found = true; } @@ -24299,6 +26968,20 @@ selection.push(item); } } + function editSelectedItem(index, selection) { + var group = selection[index]; + var editor = { + id: group.id, + submit: function submit(model) { + selection[index] = model; + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.userGroupEditor(editor); + } function removeSelectedItem(index, selection) { selection.splice(index, 1); } @@ -24378,7 +27061,6 @@ } function performDelete() { usersResource.deleteNonLoggedInUser(vm.user.id).then(function (data) { - formHelper.showNotifications(data); goToPage(vm.breadcrumbs[0]); }, function (error) { vm.deleteNotLoggedInUserButtonState = 'error'; @@ -24456,6 +27138,7 @@ function formatDatesToLocal(user) { // get current backoffice user and format dates userService.getCurrentUser().then(function (currentUser) { + currentLoggedInUser = 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'); @@ -24463,6 +27146,15 @@ user.formattedLastPasswordChangeDate = getLocalDate(user.lastPasswordChangeDate, currentUser.locale, 'LLL'); }); } + function allowGroupEdit(group) { + if (!currentLoggedInUser) { + return false; + } + if (currentLoggedInUser.userGroups.indexOf(group.alias) === -1 && currentLoggedInUser.userGroups.indexOf('admin') === -1) { + return false; + } + return true; + } init(); } angular.module('umbraco').controller('Umbraco.Editors.Users.UserController', UserEditController); @@ -24561,7 +27253,7 @@ userGroupsResource.deleteUserGroups(_.pluck(vm.selection, 'id')).then(function (data) { clearSelection(); onInit(); - }, angular.noop); + }, Utilities.noop); overlayService.close(); } }; @@ -24570,7 +27262,7 @@ } } function clearSelection() { - angular.forEach(vm.userGroups, function (userGroup) { + vm.userGroups.forEach(function (userGroup) { userGroup.selected = false; }); vm.selection = []; @@ -24588,7 +27280,16 @@ 'use strict'; (function () { 'use strict'; - function UsersController($scope, $timeout, $location, $routeParams, usersResource, userGroupsResource, userService, localizationService, usersHelper, formHelper, dateHelper, editorService, listViewHelper) { + function DetailsController($scope, externalLoginInfoService) { + var vm = this; + vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); + } + angular.module('umbraco').controller('Umbraco.Editors.Users.DetailsController', DetailsController); + }()); + 'use strict'; + (function () { + 'use strict'; + function UsersController($scope, $timeout, $location, $routeParams, usersResource, userGroupsResource, userService, localizationService, usersHelper, formHelper, dateHelper, editorService, listViewHelper, externalLoginInfoService) { var vm = this; vm.page = {}; vm.users = []; @@ -24662,27 +27363,36 @@ ]; // Get last selected layout for "users" (defaults to first layout = card layout) vm.activeLayout = listViewHelper.getLayout('users', vm.layouts); + vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); + // returns the object representing the user create button, returns null if deny local login is true + function getCreateUserButton() { + if (!vm.denyLocalLogin) { + return { + type: 'button', + labelKey: 'user_createUser', + handler: function handler() { + vm.setUsersViewState('createUser'); + } + }; + } + return null; + } + // No default buttons with denyLocalLogin // Don't show the invite button if no email is configured if (Umbraco.Sys.ServerVariables.umbracoSettings.showUserInvite) { vm.defaultButton = { + type: 'button', labelKey: 'user_inviteUser', handler: function handler() { vm.setUsersViewState('inviteUser'); } }; - vm.subButtons = [{ - labelKey: 'user_createUser', - handler: function handler() { - vm.setUsersViewState('createUser'); - } - }]; + var createUserBtn = getCreateUserButton(); + if (createUserBtn) { + vm.subButtons = [createUserBtn]; + } } else { - vm.defaultButton = { - labelKey: 'user_createUser', - handler: function handler() { - vm.setUsersViewState('createUser'); - } - }; + vm.defaultButton = getCreateUserButton(); } vm.toggleFilter = toggleFilter; vm.setUsersViewState = setUsersViewState; @@ -24801,6 +27511,7 @@ $location.search('create', 'true'); $location.search('invite', null); } else if (state === 'inviteUser') { + clearAddUserForm(); $location.search('create', null); $location.search('invite', 'true'); } else if (state === 'overview') { @@ -24831,7 +27542,7 @@ setBulkActions(vm.users); } function clearSelection() { - angular.forEach(vm.users, function (user) { + vm.users.forEach(function (user) { user.selected = false; }); vm.selection = []; @@ -24854,7 +27565,7 @@ vm.disableUserButtonState = 'busy'; usersResource.disableUsers(vm.selection).then(function (data) { // update userState - angular.forEach(vm.selection, function (userId) { + vm.selection.forEach(function (userId) { var user = getUserFromArrayById(userId, vm.users); if (user) { user.userState = 1; @@ -24872,7 +27583,7 @@ vm.enableUserButtonState = 'busy'; usersResource.enableUsers(vm.selection).then(function (data) { // update userState - angular.forEach(vm.selection, function (userId) { + vm.selection.forEach(function (userId) { var user = getUserFromArrayById(userId, vm.users); if (user) { user.userState = 0; @@ -24890,7 +27601,7 @@ vm.unlockUserButtonState = 'busy'; usersResource.unlockUsers(vm.selection).then(function (data) { // update userState - angular.forEach(vm.selection, function (userId) { + vm.selection.forEach(function (userId) { var user = getUserFromArrayById(userId, vm.users); if (user) { user.userState = 0; @@ -24928,7 +27639,7 @@ vm.selectedBulkUserGroups = []; editorService.close(); clearSelection(); - }, angular.noop); + }, Utilities.noop); }, close: function close() { vm.selectedBulkUserGroups = []; @@ -24939,7 +27650,7 @@ } function openUserGroupPicker() { var currentSelection = []; - angular.copy(vm.newUser.userGroups, currentSelection); + Utilities.copy(vm.newUser.userGroups, currentSelection); var userGroupPicker = { selection: currentSelection, submit: function submit(model) { @@ -24962,14 +27673,14 @@ function selectAll() { if (areAllSelected()) { vm.selection = []; - angular.forEach(vm.users, function (user) { + vm.users.forEach(function (user) { user.selected = false; }); } else { // clear selection so we don't add the same user twice vm.selection = []; // select all users - angular.forEach(vm.users, function (user) { + vm.users.forEach(function (user) { // prevent the current user to be selected if (!user.isCurrentUser) { user.selected = true; @@ -25009,7 +27720,7 @@ function getFilterName(array) { var name = vm.labels.all; var found = false; - angular.forEach(array, function (item) { + array.forEach(function (item) { if (item.selected) { if (!found) { name = item.name; @@ -25027,7 +27738,7 @@ } //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) { + vm.userStatesFilter.forEach(function (i) { i.selected = false; }); //we can't unselect All @@ -25035,7 +27746,7 @@ //reset the selection passed to the server vm.usersOptions.userStates = []; } else { - angular.forEach(vm.userStatesFilter, function (i) { + vm.userStatesFilter.forEach(function (i) { if (i.key === 'All') { i.selected = false; } @@ -25200,12 +27911,12 @@ }); } function setUserDisplayState(users) { - angular.forEach(users, function (user) { + users.forEach(function (user) { user.userDisplayState = usersHelper.getUserStateFromValue(user.userState); }); } function formatDates(users) { - angular.forEach(users, function (user) { + users.forEach(function (user) { if (user.lastLoginDate) { var dateVal; var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; @@ -25230,7 +27941,7 @@ vm.allowUnlockUser = true; vm.allowSetUserGroup = true; var firstSelectedUserGroups; - angular.forEach(users, function (user) { + users.forEach(function (user) { if (!user.selected) { return; } @@ -25285,6 +27996,7 @@ vm.newUser.message = ''; // clear button state vm.page.createButtonState = 'init'; + $scope.$emit('$setAccessibleHeader', true, 'general_user', false, '', '', true); } init(); } diff --git a/TestSite/Umbraco/Js/umbraco.directives.js b/TestSite/Umbraco/Js/umbraco.directives.js index 38d4983..8a45911 100644 --- a/TestSite/Umbraco/Js/umbraco.directives.js +++ b/TestSite/Umbraco/Js/umbraco.directives.js @@ -102,7 +102,7 @@ 'use strict'; (function () { 'use strict'; - function AppHeaderDirective(eventsService, appState, userService, focusService) { + function AppHeaderDirective(eventsService, appState, userService, focusService, backdropService, overlayService) { function link(scope, el, attr, ctrl) { var evts = []; // the null is important because we do an explicit bool check on this in the view @@ -125,7 +125,7 @@ scope.user = data.user; if (scope.user.avatars) { scope.avatar = []; - if (angular.isArray(scope.user.avatars)) { + if (Utilities.isArray(scope.user.avatars)) { for (var i = 0; i < scope.user.avatars.length; i++) { scope.avatar.push({ value: scope.user.avatars[i] }); } @@ -137,7 +137,7 @@ scope.user = data; if (scope.user.avatars) { scope.avatar = []; - if (angular.isArray(scope.user.avatars)) { + if (Utilities.isArray(scope.user.avatars)) { for (var i = 0; i < scope.user.avatars.length; i++) { scope.avatar.push({ value: scope.user.avatars[i] }); } @@ -161,26 +161,22 @@ appState.setDrawerState('showDrawer', drawer.show); }; scope.avatarClick = function () { - if (!scope.userDialog) { - scope.userDialog = { - view: 'user', - show: true, - close: function close(oldModel) { - scope.userDialog.show = false; - scope.userDialog = null; - } - }; - } else { - scope.userDialog.show = false; - scope.userDialog = null; - } + var dialog = { + view: 'user', + position: 'right', + name: 'overlay-user', + close: function close() { + overlayService.close(); + } + }; + overlayService.open(dialog); }; } var directive = { transclude: true, restrict: 'E', replace: true, - template: '
', + template: '
', link: link, scope: {} }; @@ -209,7 +205,7 @@ scope.loading = true; $timeout(function () { // The element to highlight - var highlightElement = angular.element(scope.highlightElement); + var highlightElement = $(scope.highlightElement); if (highlightElement && highlightElement.length > 0) { var offset = highlightElement.offset(); var width = highlightElement.outerWidth(); @@ -300,7 +296,15 @@ angular.module('umbraco.directives').directive('umbBackdrop', BackdropDirective); }()); 'use strict'; - angular.module('umbraco.directives').directive('umbContextMenu', function (navigationService, keyboardService) { + /** +* @ngdoc directive +* @name umbraco.directives.directive:umbContextMenu +* @restrict A + * + * @description + * Handles the click events on the context menu +**/ + angular.module('umbraco.directives').directive('umbContextMenu', function (navigationService, keyboardService, backdropService) { return { scope: { menuDialogTitle: '@', @@ -310,7 +314,7 @@ }, restrict: 'E', replace: true, - template: '

{{menuDialogTitle}}

', + template: ' ', link: function link(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. @@ -390,7 +394,7 @@ The drawer component is a global component and is already added to the umbraco m
  • {@link umbraco.directives.directive:umbDrawerView umbDrawerView}
  • {@link umbraco.directives.directive:umbDrawerHeader umbDrawerHeader}
  • -
  • {@link umbraco.directives.directive:umbDrawerView umbDrawerContent}
  • +
  • {@link umbraco.directives.directive:umbDrawerContent umbDrawerContent}
  • {@link umbraco.directives.directive:umbDrawerFooter umbDrawerFooter}
@@ -650,7 +654,7 @@ Use this directive to render drawer view (function () { 'use strict'; angular.module('umbraco.directives').component('umbLogin', { - template: ' ', + template: ' ', controller: UmbLoginController, controllerAs: 'vm', bindings: { @@ -658,7 +662,7 @@ Use this directive to render drawer view onLogin: '&' } }); - function UmbLoginController($scope, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, $q, $route) { + function UmbLoginController($scope, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, externalLoginInfoService, resetPasswordCodeInfo, authResource, $q) { var vm = this; vm.invitedUser = null; vm.invitedUserPasswordModel = { @@ -680,9 +684,18 @@ Use this directive to render drawer view vm.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.canSendRequiredEmail && Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; vm.errorMsg = ''; vm.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; - vm.externalLoginProviders = externalLoginInfo.providers; + vm.externalLoginProviders = externalLoginInfoService.getLoginProviders(); + vm.externalLoginProviders.forEach(function (x) { + x.customView = externalLoginInfoService.getLoginProviderView(x); + // if there are errors set for this specific provider than assign them directly to the model + if (externalLoginInfo.errorProvider === x.authType) { + x.errors = externalLoginInfo.errors; + } + }); + vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); vm.externalLoginInfo = externalLoginInfo; vm.resetPasswordCodeInfo = resetPasswordCodeInfo; + vm.logoImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginLogoImage; vm.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail; vm.$onInit = onInit; @@ -696,15 +709,21 @@ Use this directive to render drawer view vm.loginSubmit = loginSubmit; vm.requestPasswordResetSubmit = requestPasswordResetSubmit; vm.setPasswordSubmit = setPasswordSubmit; + vm.newPasswordKeyUp = newPasswordKeyUp; vm.labels = {}; localizationService.localizeMany([ vm.usernameIsEmail ? 'general_email' : 'general_username', - vm.usernameIsEmail ? 'placeholders_email' : 'placeholders_usernameHint' + vm.usernameIsEmail ? 'placeholders_email' : 'placeholders_usernameHint', + vm.usernameIsEmail ? 'placeholders_emptyEmail' : 'placeholders_emptyUsername', + 'placeholders_emptyPassword' ]).then(function (data) { vm.labels.usernameLabel = data[0]; vm.labels.usernamePlaceholder = data[1]; + vm.labels.usernameError = data[2]; + vm.labels.passwordError = data[3]; }); vm.twoFactor = {}; + vm.loginSuccess = loginSuccess; function onInit() { // Check if it is a new user var inviteVal = $location.search().invite; @@ -765,17 +784,28 @@ Use this directive to render drawer view } } function inviteSavePassword() { - if (formHelper.submitForm({ scope: $scope })) { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: vm.inviteUserPasswordForm + })) { vm.invitedUserPasswordModel.buttonState = 'busy'; currentUserResource.performSetInvitedUserPassword(vm.invitedUserPasswordModel.password).then(function (data) { //success - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ + scope: $scope, + formCtrl: vm.inviteUserPasswordForm + }); vm.invitedUserPasswordModel.buttonState = 'success'; //set the user and set them as logged in vm.invitedUser = data; userService.setAuthenticationSuccessful(data); vm.inviteStep = 2; }, function (err) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true, + formCtrl: vm.inviteUserPasswordForm + }); formHelper.handleError(err); vm.invitedUserPasswordModel.buttonState = 'error'; }); @@ -800,57 +830,65 @@ Use this directive to render drawer view vm.view = 'set-password'; SetTitle(); } - function loginSubmit() { - // make sure that we are returning to the login view. - vm.view = 'login'; - // 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 (vm.login && vm.password && vm.login.length > 0 && vm.password.length > 0) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); - } - if (vm.loginForm.$invalid) { - return; + function loginSuccess() { + vm.loginStates.submitButton = 'success'; + userService._retryRequestQueue(true); + if (vm.onLogin) { + vm.onLogin(); } - vm.loginStates.submitButton = 'busy'; - userService.authenticate(vm.login, vm.password).then(function (data) { - vm.loginStates.submitButton = 'success'; - userService._retryRequestQueue(true); - if (vm.onLogin) { - vm.onLogin(); - } - }, function (reason) { - //is Two Factor required? - if (reason.status === 402) { - vm.errorMsg = 'Additional authentication required'; - show2FALoginDialog(reason.data.twoFactorView); - } else { - vm.loginStates.submitButton = 'error'; - vm.errorMsg = reason.errorMsg; - //set the form inputs to invalid - vm.loginForm.username.$setValidity('auth', false); - vm.loginForm.password.$setValidity('auth', false); - } - userService._retryRequestQueue(); - }); - //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. - vm.loginForm.username.$viewChangeListeners.push(function () { - if (vm.loginForm.$invalid) { + } + function loginSubmit() { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: vm.loginForm + })) { + //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 (vm.login && vm.password && vm.login.length > 0 && vm.password.length > 0) { vm.loginForm.username.$setValidity('auth', true); vm.loginForm.password.$setValidity('auth', true); } - }); - vm.loginForm.password.$viewChangeListeners.push(function () { if (vm.loginForm.$invalid) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); + SetTitle(); + return; } - }); + // make sure that we are returning to the login view. + vm.view = 'login'; + vm.loginStates.submitButton = 'busy'; + userService.authenticate(vm.login, vm.password).then(function (data) { + loginSuccess(); + }, function (reason) { + //is Two Factor required? + if (reason.status === 402) { + vm.errorMsg = 'Additional authentication required'; + show2FALoginDialog(reason.data.twoFactorView); + } else { + vm.loginStates.submitButton = 'error'; + vm.errorMsg = reason.errorMsg; + //set the form inputs to invalid + vm.loginForm.username.$setValidity('auth', false); + vm.loginForm.password.$setValidity('auth', false); + } + userService._retryRequestQueue(); + }); + //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. + vm.loginForm.username.$viewChangeListeners.push(function () { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + }); + vm.loginForm.password.$viewChangeListeners.push(function () { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + }); + } } function requestPasswordResetSubmit(email) { // TODO: Do validation properly like in the invite password update @@ -859,6 +897,7 @@ Use this directive to render drawer view } vm.showEmailResetConfirmation = false; if (vm.requestPasswordResetForm.$invalid) { + vm.errorMsg = 'Email address cannot be empty'; return; } vm.errorMsg = ''; @@ -911,6 +950,9 @@ Use this directive to render drawer view } }); } + function newPasswordKeyUp(event) { + vm.passwordVal = event.target.value; + } //// function setGreeting() { var date = new Date(); @@ -1020,18 +1062,78 @@ Use this directive to render drawer view // restrict to an element replace: true, // replace the html element with the template - template: '
' + template: '
' }; } angular.module('umbraco.directives').directive('umbNavigation', umbNavigationDirective); 'use strict'; + (function () { + 'use strict'; + angular.module('umbraco.directives').component('umbPasswordTip', { + controller: UmbPasswordTipController, + controllerAs: 'vm', + template: '', + bindings: { + passwordVal: '<', + minPwdLength: '<', + minPwdNonAlphaNum: '<' + } + }); + function UmbPasswordTipController(localizationService) { + var defaultMinPwdLength = Umbraco.Sys.ServerVariables.umbracoSettings.minimumPasswordLength; + var defaultMinPwdNonAlphaNum = Umbraco.Sys.ServerVariables.umbracoSettings.minimumPasswordNonAlphaNum; + var vm = this; + vm.passwordNonAlphaTip = ''; + vm.passwordTip = ''; + vm.passwordLength = 0; + vm.$onInit = onInit; + vm.$onChanges = onChanges; + function onInit() { + if (vm.minPwdLength === undefined) { + vm.minPwdLength = defaultMinPwdLength; + } + if (vm.minPwdNonAlphaNum === undefined) { + vm.minPwdNonAlphaNum = defaultMinPwdNonAlphaNum; + } + if (vm.minPwdNonAlphaNum > 0) { + localizationService.localize('user_newPasswordFormatNonAlphaTip', [vm.minPwdNonAlphaNum]).then(function (data) { + vm.passwordNonAlphaTip = data; + updatePasswordTip(vm.passwordLength); + }); + } else { + vm.passwordNonAlphaTip = ''; + updatePasswordTip(vm.passwordLength); + } + } + function onChanges(simpleChanges) { + if (simpleChanges.passwordVal) { + vm.passwordLength = simpleChanges.passwordVal.currentValue ? simpleChanges.passwordVal.currentValue.length : 0; + updatePasswordTip(vm.passwordLength); + } + } + var updatePasswordTip = function updatePasswordTip(passwordLength) { + var remainingLength = vm.minPwdLength - passwordLength; + if (remainingLength > 0) { + localizationService.localize('user_newPasswordFormatLengthTip', [remainingLength]).then(function (data) { + vm.passwordTip = data; + if (vm.passwordNonAlphaTip) { + vm.passwordTip += '
'.concat(vm.passwordNonAlphaTip); + } + }); + } else { + vm.passwordTip = vm.passwordNonAlphaTip; + } + }; + } + }()); + 'use strict'; (function () { 'use strict'; /** * A component to render the pop up search field */ var umbSearch = { - template: ' ', + template: ' ', controllerAs: 'vm', controller: umbSearchController, bindings: { onClose: '&' } @@ -1134,7 +1236,7 @@ Use this directive to render drawer view } } $timeout(function () { - var resultElementLink = angular.element('.umb-search-item[active-result=\'true\'] .umb-search-result__link'); + var resultElementLink = $('.umb-search-item[active-result=\'true\'] .umb-search-result__link'); resultElementLink[0].focus(); }); } @@ -1160,7 +1262,8 @@ Use this directive to render drawer view searchService.searchAll(search).then(function (result) { //result is a dictionary of group Title and it's results var filtered = {}; - _.each(result, function (value, key) { + Object.keys(result).forEach(function (key) { + var value = result[key]; if (value.results.length > 0) { filtered[key] = value; } @@ -1189,28 +1292,25 @@ Use this directive to render drawer view // restrict to an element replace: true, // replace the html element with the template - template: ' ', + template: ' ', link: function link(scope, element, attr, ctrl) { var sectionItemsWidth = []; var evts = []; - var maxSections = 8; //setup scope vars - scope.maxSections = maxSections; - scope.overflowingSections = 0; scope.sections = []; + scope.visibleSections = 0; scope.currentSection = appState.getSectionState('currentSection'); scope.showTray = false; - //appState.getGlobalState("showTray"); scope.stickyNavigation = appState.getGlobalState('stickyNavigation'); - scope.needTray = false; function loadSections() { sectionService.getSectionsForUser().then(function (result) { scope.sections = result; + scope.visibleSections = scope.sections.length; // store the width of each section so we can hide/show them based on browser width // we store them because the sections get removed from the dom and then we // can't tell when to show them gain $timeout(function () { - $('#applications .sections li').each(function (index) { + $('#applications .sections li:not(:last)').each(function (index) { sectionItemsWidth.push($(this).outerWidth()); }); }); @@ -1220,23 +1320,19 @@ Use this directive to render drawer view function calculateWidth() { $timeout(function () { //total width minus room for avatar, search, and help icon - var windowWidth = $(window).width() - 150; + var containerWidth = $('.umb-app-header').outerWidth() - $('.umb-app-header__actions').outerWidth(); + var trayToggleWidth = $('#applications .sections li.expand').outerWidth(); var sectionsWidth = 0; - scope.totalSections = scope.sections.length; - scope.maxSections = maxSections; - scope.overflowingSections = scope.maxSections - scope.totalSections; - scope.needTray = scope.sections.length > scope.maxSections; // detect how many sections we can show on the screen for (var i = 0; i < sectionItemsWidth.length; i++) { var sectionItemWidth = sectionItemsWidth[i]; sectionsWidth += sectionItemWidth; - if (sectionsWidth > windowWidth) { - scope.needTray = true; - scope.maxSections = i - 1; - scope.overflowingSections = scope.maxSections - scope.totalSections; - break; + if (sectionsWidth + trayToggleWidth > containerWidth) { + scope.visibleSections = i; + return; } } + scope.visibleSections = scope.sections.length; }); } //Listen for global state changes @@ -1293,6 +1389,12 @@ Use this directive to render drawer view navigationService.showTray(); } }; + scope.currentSectionInOverflow = function () { + var currentSection = scope.sections.filter(function (s) { + return s.alias === scope.currentSection; + }); + return currentSection.length > 0 && scope.sections.indexOf(currentSection[0]) > scope.visibleSections - 1; + }; loadSections(); } }; @@ -1606,7 +1708,7 @@ In the following example you see how to run some custom logic before a step goes setPopoverPosition(null); return; } - var element = angular.element(scope.model.currentStep.element); + var element = $(scope.model.currentStep.element); // we couldn't find the element in the dom - abort and show error if (element.length === 0) { scope.elementNotFound = true; @@ -1651,8 +1753,8 @@ In the following example you see how to run some custom logic before a step goes var popoverWidth = popover.outerWidth(); var popoverHeight = popover.outerHeight(); var popoverOffset = popover.offset(); - var documentWidth = angular.element(document).width(); - var documentHeight = angular.element(document).height(); + var documentWidth = $(document).width(); + var documentHeight = $(document).height(); if (element) { var offset = element.offset(); var width = element.outerWidth(); @@ -1792,11 +1894,11 @@ In the following example you see how to run some custom logic before a step goes var eventName = scope.model.currentStep.event + '.step-' + scope.model.currentStepIndex; var removeEventName = 'remove.step-' + scope.model.currentStepIndex; if (scope.model.currentStep.eventElement) { - angular.element(scope.model.currentStep.eventElement).off(eventName); - angular.element(scope.model.currentStep.eventElement).off(removeEventName); + $(scope.model.currentStep.eventElement).off(eventName); + $(scope.model.currentStep.eventElement).off(removeEventName); } else { - angular.element(scope.model.currentStep.element).off(eventName); - angular.element(scope.model.currentStep.element).off(removeEventName); + $(scope.model.currentStep.element).off(eventName); + $(scope.model.currentStep.element).off(removeEventName); } } function resize() { @@ -1814,7 +1916,7 @@ In the following example you see how to run some custom logic before a step goes transclude: true, restrict: 'E', replace: true, - template: '

Congratulations!

You have reached the end of the {{model.name}} tour - way to go!

Oh, we got lost!

We lost the next step {{ model.currentStep.title }} and don\'t know where to go.

Please go back and start the tour again.

', + template: '

Congratulations!

You have reached the end of the {{model.name}} tour - way to go!

Oh, we got lost!

We lost the next step {{ model.currentStep.title }} and don\'t know where to go.

Please go back and start the tour again.

', link: link, scope: { model: '=' } }; @@ -1850,7 +1952,7 @@ In the following example you see how to run some custom logic before a step goes restrict: 'E', replace: true, transclude: true, - template: '
', + template: '
', scope: { size: '@?', onClose: '&?', @@ -2032,33 +2134,34 @@ Use this directive to render an umbraco button. The directive can be used to gen @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=} href Url/Path to navigato to. (requires "type" to be set to "link") +@param {string=} type Set the button type ("button", "link", "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=} 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 {string=} size Set a button size ("xs", "m", "l", "xl"). @param {boolean=} disabled Set to true to disable the button. -@param {string=} addEllipsis Adds an ellipsis character (…) to the button label which means the button will open a dialog or prompt the user for more information. -@param {string=} showCaret Shows a caret on the right side of the button label -@param {string=} autoFocus add autoFocus to the button -@param {string=} hasPopup Used to expose to the accessibility API whether the button will trigger a popup or not -@param {string=]} isExpanded Used to add an aria-expanded attribute and expose whether the button has expanded a popup or not +@param {boolean=} addEllipsis Adds an ellipsis character (…) to the button label which means the button will open a dialog or prompt the user for more information. +@param {boolean=} showCaret Shows a caret on the right side of the button label +@param {boolean=} autoFocus add autoFocus to the button +@param {boolean=} hasPopup Used to expose to the accessibility API whether the button will trigger a popup or not +@param {boolean=} isExpanded Used to add an aria-expanded attribute and expose whether the button controls collapsible content **/ (function () { 'use strict'; angular.module('umbraco.directives').component('umbButton', { transclude: true, - template: '
{{vm.buttonLabel}}
', + template: '
{{vm.buttonLabel}}
', controller: UmbButtonController, controllerAs: 'vm', bindings: { action: '&?', href: '@?', + hrefTarget: '@?', type: '@', buttonStyle: '@?', state: 'Added in Umbraco version 8.7.0 Use this directive to render an umbraco ellipsis. + +

Markup example

+
+    
+ + + + +
+
+ +@param {callback} action Callback when the value of the checkbox changes through interaction. +@param {string} text Set the text for the checkbox label. +@param {string=} labelKey Set a dictionary/localization string for the checkbox label +@param {string=} cssClass Set a css class modifier +@param {string=} color Set a hex code e.g. #f5c1bc. #000000 by default +@param {boolean=} showText Set to true to show the text. false by default +@param {string=} element Highlights a DOM-element (HTML selector) e.g. "my-div-name" +@param {string=} state Set the initial state of the component. To have it hidden use hidden +@param {string=} mode Set the mode, which decides how to style the component. "small" and "tab" are currently supported +**/ + (function () { + 'use strict'; + function UmbButtonEllipsis($timeout, localizationService) { + var vm = this; + vm.$onInit = onInit; + vm.clickButton = clickButton; + function onInit() { + setText(); + setColor(); + } + function clickButton(event) { + if (vm.action) { + vm.action({ $event: event }); + } + } + function setText() { + if (vm.labelKey) { + localizationService.localize(vm.labelKey).then(function (data) { + // If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [ + if (data.indexOf('[') === -1) { + vm.text = data; + } + }); + } + } + function setColor() { + vm.color = vm.color ? vm.color : '#000000'; + } + } + var component = { + template: ' ', + controller: UmbButtonEllipsis, + controllerAs: 'vm', + transclude: true, + bindings: { + text: '@', + labelKey: '@?', + action: '&', + cssClass: '@?', + color: '@?', + showText: ' ', + template: '
', scope: { defaultButton: '=', subButtons: '=', @@ -2374,27 +2568,29 @@ Use this directive to render a button with a dropdown of alternative actions. function onInit() { scope.inputId = scope.inputId || 'umb-toggle_' + String.CreateGuid(); setLabelText(); - // must wait until the current digest cycle is finished before we emit this event on init, + // Must wait until the current digest cycle is finished before we emit this event on init, // otherwise other property editors might not yet be ready to receive the event $timeout(function () { eventsService.emit('toggleValue', { value: scope.checked }); }, 100); } 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; - }); } - // set default label for "Off" if (scope.labelOff) { scope.displayLabelOff = scope.labelOff; - } else { - localizationService.localize('general_off').then(function (value) { - scope.displayLabelOff = value; + } + if (scope.displayLabelOn.length === 0 && scope.displayLabelOff.length === 0) { + var labelKeys = [ + 'general_on', + 'general_off' + ]; + localizationService.localizeMany(labelKeys).then(function (data) { + // Set default label for "On" + scope.displayLabelOn = data[0]; + // Set default label for "Off" + scope.displayLabelOff = data[1]; }); } } @@ -2409,8 +2605,10 @@ Use this directive to render a button with a dropdown of alternative actions. var directive = { restrict: 'E', replace: true, - template: ' ', + template: ' ', scope: { + // TODO: This should have required ngModel so we can track and validate user input correctly + // https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#custom-control-example checked: '=', disabled: '=', inputId: '@', @@ -2498,6 +2696,9 @@ Use this directive to render a group of toggle buttons. 'use strict'; function ToggleGroupDirective() { function link(scope, el, attr, ctrl) { + for (var i = 0; i < scope.items.length; i++) { + scope.items[i].inputId = 'umb-toggle-group-item_' + String.CreateGuid(); + } scope.change = function (item) { if (item.disabled) { return; @@ -2511,7 +2712,7 @@ Use this directive to render a group of toggle buttons. var directive = { restrict: 'E', replace: true, - template: '
{{ item.name }}
{{ item.description }}
', + template: '
{{ item.description }}
', scope: { items: '=', onClick: '&' @@ -2525,7 +2726,7 @@ Use this directive to render a group of toggle buttons. 'use strict'; (function () { 'use strict'; - function ContentEditController($rootScope, $scope, $routeParams, $q, $window, appState, contentResource, entityResource, navigationService, notificationsService, serverValidationManager, contentEditingHelper, localizationService, formHelper, umbRequestHelper, editorState, $http, eventsService, overlayService, $location, localStorageService, treeService) { + function ContentEditController($rootScope, $scope, $routeParams, $q, $window, appState, contentResource, entityResource, navigationService, notificationsService, contentAppHelper, serverValidationManager, contentEditingHelper, localizationService, formHelper, umbRequestHelper, editorState, $http, eventsService, overlayService, $location, localStorageService, treeService, $exceptionHandler) { var evts = []; var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode; var watchingCulture = false; @@ -2547,14 +2748,14 @@ Use this directive to render a group of toggle buttons. $scope.page.hideActionsMenu = infiniteMode ? true : false; $scope.page.hideChangeVariant = false; $scope.allowOpen = true; - $scope.app = null; + $scope.activeApp = null; //initializes any watches function startWatches(content) { //watch for changes to isNew, set the page.isNew accordingly and load the breadcrumb if we can $scope.$watch('isNew', function (newVal, oldVal) { $scope.page.isNew = Object.toBoolean(newVal); //We fetch all ancestors of the node to generate the footer breadcrumb navigation - if (content.parentId && content.parentId !== -1) { + if (content.parentId && content.parentId !== -1 && content.parentId !== -20) { loadBreadcrumb(); if (!watchingCulture) { $scope.$watch('culture', function (value, oldValue) { @@ -2575,27 +2776,20 @@ Use this directive to render a group of toggle buttons. // we need to check wether an app is present in the current data, if not we will present the default app. var isAppPresent = false; // on first init, we dont have any apps. but if we are re-initializing, we do, but ... - if ($scope.app) { - // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) + if ($scope.activeApp) { _.forEach(content.apps, function (app) { - if (app === $scope.app) { + if (app.alias === $scope.activeApp.alias) { isAppPresent = true; + $scope.appChanged(app); } }); - // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { - _.forEach(content.apps, function (app) { - if (app.alias === $scope.app.alias) { - isAppPresent = true; - app.active = true; - $scope.appChanged(app); - } - }); + // active app does not exist anymore. + $scope.activeApp = null; } } // if we still dont have a app, lets show the first one: - if (isAppPresent === false && content.apps.length) { - content.apps[0].active = true; + if ($scope.activeApp === null && content.apps.length) { $scope.appChanged(content.apps[0]); } // otherwise make sure the save options are up to date with the current content state else { @@ -2639,8 +2833,8 @@ Use this directive to render a group of toggle buttons. } } /** Returns true if the content item varies by culture */ - function isContentCultureVariant() { - return $scope.content.variants.length > 1; + function hasVariants(content) { + return content.variants.length > 1; } function reload() { $scope.page.loading = true; @@ -2692,6 +2886,12 @@ Use this directive to render a group of toggle buttons. localStorageService.clearAll(/^tinymce__/); })); } + function appendRuntimeData() { + $scope.content.variants.forEach(function (variant) { + variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); + variant.htmlId = '_content_variant_' + variant.compositeId + '_'; + }); + } /** * This does the content loading and initializes everything, called on first load */ @@ -2699,6 +2899,7 @@ Use this directive to render a group of toggle buttons. //we are editing so get the content item from the server return $scope.getMethod()($scope.contentId).then(function (data) { $scope.content = data; + appendRuntimeData(); init(); syncTreeNode($scope.content, $scope.content.path, true); resetLastListPageNumber($scope.content); @@ -2713,6 +2914,7 @@ Use this directive to render a group of toggle buttons. //we are creating so get an empty content item return $scope.getScaffoldMethod()().then(function (data) { $scope.content = data; + appendRuntimeData(); init(); startWatches($scope.content); resetLastListPageNumber($scope.content); @@ -2726,11 +2928,15 @@ Use this directive to render a group of toggle buttons. * @param {any} app the active content app */ function createButtons(content) { + var isBlueprint = content.isBlueprint; + if ($scope.page.isNew && $location.path().search(/contentBlueprints/i) !== -1) { + isBlueprint = true; + } // for trashed and element type items, the save button is the primary action - otherwise it's a secondary action - $scope.page.saveButtonStyle = content.trashed || content.isElement || content.isBlueprint ? 'primary' : 'info'; + $scope.page.saveButtonStyle = content.trashed || content.isElement || isBlueprint ? 'primary' : 'info'; // only create the save/publish/preview buttons if the // content app is "Conent" - if ($scope.app && $scope.app.alias !== 'umbContent' && $scope.app.alias !== 'umbInfo' && $scope.app.alias !== 'umbListView') { + if ($scope.activeApp && !contentAppHelper.isContentBasedApp($scope.activeApp)) { $scope.defaultButton = null; $scope.subButtons = null; $scope.page.showSaveButton = false; @@ -2742,6 +2948,8 @@ Use this directive to render a group of toggle buttons. $scope.page.showSaveButton = true; // add ellipsis to the save button if it opens the variant overlay $scope.page.saveButtonEllipsis = content.variants && content.variants.length > 1 ? 'true' : 'false'; + } else { + $scope.page.showSaveButton = false; } // create the pubish combo button $scope.page.buttonGroupState = 'init'; @@ -2881,7 +3089,8 @@ Use this directive to render a group of toggle buttons. create: $scope.page.isNew, action: args.action, showNotifications: args.showNotifications, - softRedirect: true + softRedirect: true, + skipValidation: args.skipValidation }).then(function (data) { //success init(); @@ -2890,14 +3099,27 @@ Use this directive to render a group of toggle buttons. syncTreeNode($scope.content, data.path, false, args.reloadChildren); eventsService.emit('content.saved', { content: $scope.content, - action: args.action + action: args.action, + valid: true }); - resetNestedFieldValiation(fieldsToRollback); + if ($scope.contentForm.$invalid !== true) { + resetNestedFieldValiation(fieldsToRollback); + } ensureDirtyIsSetIfAnyVariantIsDirty(); return $q.when(data); }, function (err) { syncTreeNode($scope.content, $scope.content.path); - resetNestedFieldValiation(fieldsToRollback); + if ($scope.contentForm.$invalid !== true) { + resetNestedFieldValiation(fieldsToRollback); + } + if (err && err.status === 400 && err.data) { + // content was saved but is invalid. + eventsService.emit('content.saved', { + content: $scope.content, + action: args.action, + valid: false + }); + } return $q.reject(err); }); } @@ -2938,6 +3160,11 @@ Use this directive to render a group of toggle buttons. } } } + function handleHttpException(err) { + if (err && !err.status) { + $exceptionHandler(err); + } + } /** Just shows a simple notification that there are client side validation issues to be fixed */ function showValidationNotification() { //TODO: We need to make the validation UI much better, there's a lot of inconsistencies in v8 including colors, issues with the property groups and validation errors between variants @@ -2975,6 +3202,7 @@ Use this directive to render a group of toggle buttons. //set a model property for the dialog skipFormValidation: true, //when submitting the overlay form, skip any client side validation + includeUnpublished: false, submitButtonLabelKey: 'content_unpublish', submitButtonStyle: 'warning', submit: function submit(model) { @@ -2995,7 +3223,12 @@ Use this directive to render a group of toggle buttons. eventsService.emit('content.unpublished', { content: $scope.content }); overlayService.close(); }, function (err) { + formHelper.resetForm({ + scope: $scope, + hasErrors: true + }); $scope.page.buttonGroupState = 'error'; + handleHttpException(err); }); }, close: function close() { @@ -3007,7 +3240,7 @@ Use this directive to render a group of toggle buttons. }; $scope.sendToPublish = function () { clearNotifications($scope.content); - if (isContentCultureVariant()) { + if (hasVariants($scope.content)) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, @@ -3040,8 +3273,7 @@ Use this directive to render a group of toggle buttons. model.submitButtonState = 'error'; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - //don't reject, we've handled the error - return $q.when(err); + handleHttpException(err); }); }, close: function close() { @@ -3059,15 +3291,16 @@ Use this directive to render a group of toggle buttons. action: 'sendToPublish' }).then(function () { $scope.page.buttonGroupState = 'success'; - }, function () { + }, function (err) { $scope.page.buttonGroupState = 'error'; + handleHttpException(err); }); ; } }; $scope.saveAndPublish = function () { clearNotifications($scope.content); - if (isContentCultureVariant()) { + if (hasVariants($scope.content)) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, @@ -3100,8 +3333,7 @@ Use this directive to render a group of toggle buttons. model.submitButtonState = 'error'; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - //don't reject, we've handled the error - return $q.when(err); + handleHttpException(err); }); }, close: function close() { @@ -3122,70 +3354,76 @@ Use this directive to render a group of toggle buttons. action: 'publish' }).then(function () { $scope.page.buttonGroupState = 'success'; - }, function () { + }, function (err) { $scope.page.buttonGroupState = 'error'; + handleHttpException(err); }); } }; $scope.save = function () { clearNotifications($scope.content); // TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant - if (isContentCultureVariant()) { - //before we launch the dialog we want to execute all client side validations first - if (formHelper.submitForm({ - scope: $scope, - action: 'openSaveDialog' - })) { - var dialog = { - parentScope: $scope, - view: 'views/content/overlays/save.html', - variants: $scope.content.variants, - //set a model property for the dialog - skipFormValidation: true, - //when submitting the overlay form, skip any client side validation - submitButtonLabelKey: 'buttons_save', - submit: function submit(model) { - model.submitButtonState = 'busy'; + if (hasVariants($scope.content)) { + var dialog = { + parentScope: $scope, + view: 'views/content/overlays/save.html', + variants: $scope.content.variants, + //set a model property for the dialog + skipFormValidation: true, + //when submitting the overlay form, skip any client side validation + submitButtonLabelKey: 'buttons_save', + submit: function submit(model) { + model.submitButtonState = 'busy'; + clearNotifications($scope.content); + //we need to return this promise so that the dialog can handle the result and wire up the validation response + return performSave({ + saveMethod: $scope.saveMethod(), + action: 'save', + showNotifications: false, + skipValidation: true + }).then(function (data) { + //show all notifications manually here since we disabled showing them automatically in the save method + formHelper.showNotifications(data); clearNotifications($scope.content); - //we need to return this promise so that the dialog can handle the result and wire up the validation response - return performSave({ - saveMethod: $scope.saveMethod(), - action: 'save', - showNotifications: false - }).then(function (data) { - //show all notifications manually here since we disabled showing them automatically in the save method - formHelper.showNotifications(data); - clearNotifications($scope.content); - overlayService.close(); - return $q.when(data); - }, function (err) { - clearDirtyState($scope.content.variants); - model.submitButtonState = 'error'; - //re-map the dialog model since we've re-bound the properties - dialog.variants = $scope.content.variants; - //don't reject, we've handled the error - return $q.when(err); - }); - }, - close: function close(oldModel) { overlayService.close(); - } - }; - overlayService.open(dialog); - } else { - showValidationNotification(); - } + return $q.when(data); + }, function (err) { + clearDirtyState($scope.content.variants); + //model.submitButtonState = "error"; + // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. + if (err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { + model.submitButtonState = 'success'; + } else { + model.submitButtonState = 'error'; + } + //re-map the dialog model since we've re-bound the properties + dialog.variants = $scope.content.variants; + handleHttpException(err); + }); + }, + close: function close(oldModel) { + overlayService.close(); + } + }; + overlayService.open(dialog); } else { //ensure the flags are set $scope.content.variants[0].save = true; $scope.page.saveButtonState = 'busy'; return performSave({ saveMethod: $scope.saveMethod(), - action: 'save' + action: 'save', + skipValidation: true }).then(function () { $scope.page.saveButtonState = 'success'; - }, function () { - $scope.page.saveButtonState = 'error'; + }, function (err) { + // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. + if (err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { + $scope.page.saveButtonState = 'success'; + } else { + $scope.page.saveButtonState = 'error'; + } + handleHttpException(err); }); } }; @@ -3196,14 +3434,14 @@ Use this directive to render a group of toggle buttons. scope: $scope, action: 'schedule' })) { - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { //ensure the flags are set $scope.content.variants[0].save = true; } var dialog = { parentScope: $scope, view: 'views/content/overlays/schedule.html', - variants: angular.copy($scope.content.variants), + variants: Utilities.copy($scope.content.variants), //set a model property for the dialog skipFormValidation: true, //when submitting the overlay form, skip any client side validation @@ -3232,14 +3470,13 @@ Use this directive to render a group of toggle buttons. }, function (err) { clearDirtyState($scope.content.variants); //if this is invariant, show the notification errors, else they'll be shown inline with the variant - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { formHelper.showNotifications(err.data); } model.submitButtonState = 'error'; //re-map the dialog model since we've re-bound the properties - dialog.variants = angular.copy($scope.content.variants); - //don't reject, we've handled the error - return $q.when(err); + dialog.variants = Utilities.copy($scope.content.variants); + handleHttpException(err); }); }, close: function close() { @@ -3258,7 +3495,7 @@ Use this directive to render a group of toggle buttons. scope: $scope, action: 'publishDescendants' })) { - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { //ensure the flags are set $scope.content.variants[0].save = true; $scope.content.variants[0].publish = true; @@ -3291,14 +3528,13 @@ Use this directive to render a group of toggle buttons. }, function (err) { clearDirtyState($scope.content.variants); //if this is invariant, show the notification errors, else they'll be shown inline with the variant - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { formHelper.showNotifications(err.data); } model.submitButtonState = 'error'; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - //don't reject, we've handled the error - return $q.when(err); + handleHttpException(err); }); }, close: function close() { @@ -3378,9 +3614,15 @@ Use this directive to render a group of toggle buttons. * Call back when a content app changes * @param {any} app */ - $scope.appChanged = function (app) { - $scope.app = app; - $scope.$broadcast('editors.apps.appChanged', { app: app }); + $scope.appChanged = function (activeApp) { + $scope.activeApp = activeApp; + _.forEach($scope.content.apps, function (app) { + app.active = false; + if (app.alias === $scope.activeApp.alias) { + app.active = true; + } + }); + $scope.$broadcast('editors.apps.appChanged', { app: activeApp }); createButtons($scope.content); }; /** @@ -3425,7 +3667,7 @@ Use this directive to render a group of toggle buttons. var directive = { restrict: 'E', replace: true, - template: '
', + template: '
', controller: 'Umbraco.Editors.Content.EditorDirectiveController', scope: { contentId: '=', @@ -3436,6 +3678,7 @@ Use this directive to render a group of toggle buttons. getMethod: '&', getScaffoldMethod: '&?', culture: '=?', + segment: '=?', infiniteModel: '=?' } }; @@ -3445,6 +3688,44 @@ Use this directive to render a group of toggle buttons. angular.module('umbraco.directives').directive('contentEditor', createDirective); }()); 'use strict'; + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); + } + function _nonIterableRest() { + throw new TypeError('Invalid attempt to destructure non-iterable instance'); + } + function _iterableToArrayLimit(arr, i) { + if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === '[object Arguments]')) { + return; + } + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + if (i && _arr.length === i) + break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i['return'] != null) + _i['return'](); + } finally { + if (_d) + throw _e; + } + } + return _arr; + } + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) + return arr; + } (function () { 'use strict'; function ContentNodeInfoDirective($timeout, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource, overlayService, entityResource) { @@ -3460,6 +3741,7 @@ Use this directive to render a group of toggle buttons. scope.allowChangeDocumentType = false; scope.allowChangeTemplate = false; scope.allTemplates = []; + scope.historyLabelKey = scope.node.variants && scope.node.variants.length === 1 ? 'general_history' : 'auditTrails_historyIncludingVariants'; function onInit() { entityResource.getAll('Template').then(function (templates) { scope.allTemplates = templates; @@ -3485,23 +3767,20 @@ Use this directive to render a group of toggle buttons. 'content_notCreated', 'prompt_unsavedChanges', 'prompt_doctypeChangeWarning', - 'general_history', - 'auditTrails_historyIncludingVariants', 'content_itemNotPublished', 'general_choose' ]; localizationService.localizeMany(keys).then(function (data) { - labels.deleted = data[0]; - labels.unpublished = data[1]; - //aka draft - labels.published = data[2]; - labels.publishedPendingChanges = data[3]; - labels.notCreated = data[4]; - labels.unsavedChanges = data[5]; - labels.doctypeChangeWarning = data[6]; - labels.notPublished = data[9]; - scope.historyLabel = scope.node.variants && scope.node.variants.length === 1 ? data[7] : data[8]; - scope.chooseLabel = data[10]; + var _data = _slicedToArray(data, 9); + labels.deleted = _data[0]; + labels.unpublished = _data[1]; + labels.published = _data[2]; + labels.publishedPendingChanges = _data[3]; + labels.notCreated = _data[4]; + labels.unsavedChanges = _data[5]; + labels.doctypeChangeWarning = _data[6]; + labels.notPublished = _data[7]; + scope.chooseLabel = _data[8]; setNodePublishStatus(); if (scope.currentUrls && scope.currentUrls.length === 0) { if (scope.node.id > 0) { @@ -3557,6 +3836,7 @@ Use this directive to render a group of toggle buttons. view: 'default', content: labels.doctypeChangeWarning, submitButtonLabelKey: 'general_continue', + submitButtonStyle: 'warning', closeButtonLabelKey: 'general_cancel', submit: function submit() { openDocTypeEditor(documentType); @@ -3626,7 +3906,7 @@ Use this directive to render a group of toggle buttons. logResource.getPagedEntityLog(scope.auditTrailOptions).then(function (data) { // get current backoffice user and format dates userService.getCurrentUser().then(function (currentUser) { - angular.forEach(data.items, function (item) { + Utilities.forEach(data.items, function (item) { item.timestampFormatted = dateHelper.getLocalDate(item.timestamp, currentUser.locale, 'LLL'); }); }); @@ -3642,7 +3922,7 @@ Use this directive to render a group of toggle buttons. } function loadRedirectUrls() { scope.loadingRedirectUrls = true; - //check if Redirect Url Management is enabled + //check if Redirect URL Management is enabled redirectUrlsResource.getEnableState().then(function (response) { scope.urlTrackerDisabled = response.enabled !== true; if (scope.urlTrackerDisabled === false) { @@ -3657,7 +3937,7 @@ Use this directive to render a group of toggle buttons. }); } function setAuditTrailLogTypeColor(auditTrail) { - angular.forEach(auditTrail, function (item) { + Utilities.forEach(auditTrail, function (item) { switch (item.logType) { case 'Save': item.logTypeColor = 'primary'; @@ -3709,21 +3989,20 @@ Use this directive to render a group of toggle buttons. }); } function updateCurrentUrls() { - // never show urls for element types (if they happen to have been created in the content tree) - if (scope.node.isElement) { + // never show URLs for element types (if they happen to have been created in the content tree) + if (scope.node.isElement || scope.node.urls === null) { scope.currentUrls = null; return; } // find the urls for the currently selected language - if (scope.node.variants.length > 1) { - // nodes with variants - scope.currentUrls = _.filter(scope.node.urls, function (url) { - return scope.currentVariant.language.culture === url.culture; - }); - } else { - // invariant nodes - scope.currentUrls = scope.node.urls; - } + // when there is no selected language (allow vary by culture == false), show all urls of the node. + scope.currentUrls = _.filter(scope.node.urls, function (url) { + return scope.currentVariant.language == null || scope.currentVariant.language.culture === url.culture; + }); + // figure out if multiple cultures apply across the content URLs + scope.currentUrlsHaveMultipleCultures = _.keys(_.groupBy(scope.currentUrls, function (url) { + return url.culture; + })).length > 1; } // load audit trail and redirects when on the info tab evts.push(eventsService.on('app.tabChange', function (event, args) { @@ -3767,7 +4046,7 @@ Use this directive to render a group of toggle buttons. require: '^^umbVariantContent', restrict: 'E', replace: true, - template: '

The following URLs redirect to this content item:

{{ item.userName }}
{{item.timestampFormatted}}
{{ item.logType }} {{ item.comment }}
{{currentVariant.createDateFormatted}} {{currentVariant.releaseDateFormatted}} {{currentVariant.expireDateFormatted}}
{{ node.id }}
{{ node.key }}
', + template: '

The following URLs redirect to this content item:

{{ item.userName }}
{{item.timestampFormatted}}
{{ item.logType }} {{ item.comment }}
{{currentVariant.createDateFormatted}} {{currentVariant.releaseDateFormatted}} {{currentVariant.expireDateFormatted}}
{{ node.id }}
{{ node.key }}
', scope: { node: '=' }, link: link }; @@ -3779,19 +4058,40 @@ Use this directive to render a group of toggle buttons. (function () { 'use strict'; /** This directive is used to render out the current variant tabs and properties and exposes an API for other directives to consume */ - function tabbedContentDirective($timeout) { - function link($scope, $element, $attrs) { + function tabbedContentDirective($timeout, $filter, contentEditingHelper, contentTypeHelper) { + function link($scope, $element) { var appRootNode = $element[0]; // Directive for cached property groups. var propertyGroupNodesDictionary = {}; var scrollableNode = appRootNode.closest('.umb-scrollable'); - scrollableNode.addEventListener('scroll', onScroll); - scrollableNode.addEventListener('mousewheel', cancelScrollTween); + $scope.activeTabAlias = null; + $scope.tabs = []; + $scope.$watchCollection('content.tabs', function (newValue) { + contentTypeHelper.defineParentAliasOnGroups(newValue); + contentTypeHelper.relocateDisorientedGroups(newValue); + // make a collection with only tabs and not all groups + $scope.tabs = $filter('filter')(newValue, function (tab) { + return tab.type === 1; + }); + if ($scope.tabs.length > 0) { + // if we have tabs and some groups that doesn't belong to a tab we need to render those on an "Other" tab. + contentEditingHelper.registerGenericTab(newValue); + $scope.setActiveTab($scope.tabs[0]); + scrollableNode.removeEventListener('scroll', onScroll); + scrollableNode.removeEventListener('mousewheel', cancelScrollTween); // only trigger anchor scroll when there are no tabs + } else { + scrollableNode.addEventListener('scroll', onScroll); + scrollableNode.addEventListener('mousewheel', cancelScrollTween); + } + }); function onScroll(event) { var viewFocusY = scrollableNode.scrollTop + scrollableNode.clientHeight * 0.5; for (var i in $scope.content.tabs) { var group = $scope.content.tabs[i]; var node = propertyGroupNodesDictionary[group.id]; + if (!node) { + return; + } if (viewFocusY >= node.offsetTop && viewFocusY <= node.offsetTop + node.clientHeight) { setActiveAnchor(group); return; @@ -3817,7 +4117,7 @@ Use this directive to render a group of toggle buttons. } function getScrollPositionFor(id) { if (propertyGroupNodesDictionary[id]) { - return propertyGroupNodesDictionary[id].offsetTop - 20; // currently only relative to closest relatively positioned parent + return propertyGroupNodesDictionary[id].offsetTop - 20; // currently only relative to closest relatively positioned parent } return null; } @@ -3854,9 +4154,16 @@ Use this directive to render a group of toggle buttons. $scope.registerPropertyGroup = function (element, appAnchor) { propertyGroupNodesDictionary[appAnchor] = element; }; + $scope.setActiveTab = function (tab) { + $scope.activeTabAlias = tab.alias; + $scope.tabs.forEach(function (tab) { + return tab.active = false; + }); + tab.active = true; + }; $scope.$on('editors.apps.appChanged', function ($event, $args) { // if app changed to this app, then we want to scroll to the current anchor - if ($args.app.alias === 'umbContent') { + if ($args.app.alias === 'umbContent' && $scope.tabs.length === 0) { var activeAnchor = getActiveAnchor(); $timeout(jumpTo.bind(null, [activeAnchor.id])); } @@ -3874,16 +4181,15 @@ Use this directive to render a group of toggle buttons. scrollableNode.removeEventListener('mousewheel', cancelScrollTween); }); } - function controller($scope, $element, $attrs) { + function controller($scope) { //expose the property/methods for other directives to use this.content = $scope.content; - this.activeVariant = _.find(this.content.variants, function (variant) { - return variant.active; - }); - $scope.activeVariant = this.activeVariant; - $scope.defaultVariant = _.find(this.content.variants, function (variant) { - return variant.language.isDefault; - }); + if ($scope.contentNodeModel) { + $scope.defaultVariant = _.find($scope.contentNodeModel.variants, function (variant) { + // defaultVariant will never have segment. Wether it has a language or not depends on the setup. + return !variant.segment && (variant.language && variant.language.isDefault || !variant.language); + }); + } $scope.unlockInvariantValue = function (property) { property.unlockInvariantValue = !property.unlockInvariantValue; }; @@ -3892,14 +4198,31 @@ Use this directive to render a group of toggle buttons. $scope.content.isDirty = true; } }); + $scope.propertyEditorDisabled = function (property) { + if (property.unlockInvariantValue) { + return false; + } + var contentLanguage = $scope.content.language; + var canEditCulture = !contentLanguage || // If the property culture equals the content culture it can be edited + property.culture === contentLanguage.culture || // A culture-invariant property can only be edited by the default language variant + property.culture == null && contentLanguage.isDefault; + var canEditSegment = property.segment === $scope.content.segment; + return !canEditCulture || !canEditSegment; + }; } var directive = { restrict: 'E', replace: true, - template: '
{{ group.label }}
', + template: '
{{ group.label }}
', controller: controller, link: link, - scope: { content: '=' } + scope: { + content: '=', + // in this context the content is the variant model. + contentNodeModel: '=?', + //contentNodeModel is the content model for the node, + contentApp: '=?' // contentApp is the origin app model for this view + } }; return directive; } @@ -3912,14 +4235,13 @@ Use this directive to render a group of toggle buttons. * A component to encapsulate each variant editor which includes the name header and all content apps for a given variant */ var umbVariantContent = { - template: '
This item is in the Recycle Bin
', + template: '
This item is in the Recycle Bin
', bindings: { content: '<', page: '<', editor: '<', editorIndex: '<', editorCount: '<', - openVariants: '<', onCloseSplitView: '&', onSelectVariant: '&', onOpenSplitView: '&', @@ -3931,7 +4253,7 @@ Use this directive to render a group of toggle buttons. controllerAs: 'vm', controller: umbVariantContentController }; - function umbVariantContentController($scope, $element, $location) { + function umbVariantContentController($scope, contentAppHelper) { var unsubscribe = []; var vm = this; vm.$onInit = onInit; @@ -3943,13 +4265,12 @@ Use this directive to render a group of toggle buttons. vm.selectAppAnchor = selectAppAnchor; vm.showBackButton = showBackButton; function onInit() { - // disable the name field if the active content app is not "Content" - vm.nameDisabled = false; - angular.forEach(vm.editor.content.apps, function (app) { - if (app.active && app.alias !== 'umbContent' && app.alias !== 'umbInfo' && app.alias !== 'umbListView') { - vm.nameDisabled = true; - } + // Make copy of apps, so we can have a variant specific model for the App. (needed for validation etc.) + vm.editor.variantApps = Utilities.copy(vm.content.apps); + var activeApp = vm.content.apps.find(function (app) { + return app.active; }); + onAppChanged(activeApp); } function showBackButton() { return vm.page.listViewPath !== null && vm.showBack; @@ -3957,8 +4278,8 @@ Use this directive to render a group of toggle buttons. /** Called when the component has linked all elements, this is when the form controller is available */ function postLink() { //set the content to dirty if the header changes - unsubscribe.push($scope.$watch('contentHeaderForm.$dirty', function (newValue, oldValue) { - if (newValue === true) { + unsubscribe.push($scope.$watch('vm.editor.content.name', function (newValue, oldValue) { + if (newValue !== oldValue) { vm.editor.content.isDirty = true; } })); @@ -3988,13 +4309,20 @@ Use this directive to render a group of toggle buttons. } } $scope.$on('editors.apps.appChanged', function ($event, $args) { - var app = $args.app; - // disable the name field if the active content app is not "Content" or "Info" - vm.nameDisabled = false; - if (app && app.alias !== 'umbContent' && app.alias !== 'umbInfo' && app.alias !== 'umbListView') { - vm.nameDisabled = true; - } + var activeApp = $args.app; + // sync varaintApps active with new active. + _.forEach(vm.editor.variantApps, function (app) { + app.active = app.alias === activeApp.alias; + }); + onAppChanged(activeApp); + }); + $scope.$on('listView.itemsChanged', function ($event, $args) { + vm.disableActionsMenu = $args.items.length > 0; }); + function onAppChanged(activeApp) { + // disable the name field if the active content app is not "Content" or "Info" + vm.nameDisabled = activeApp && !contentAppHelper.isContentBasedApp(activeApp); + } /** * Used to proxy a callback * @param {any} item @@ -4027,12 +4355,12 @@ Use this directive to render a group of toggle buttons. * A component for split view content editing */ var umbVariantContentEditors = { - template: '
', + template: '
', bindings: { page: '<', content: '<', - // TODO: Not sure if this should be = since we are changing the 'active' property of a variant culture: '<', + segment: '<', onSelectApp: '&?', onSelectAppAnchor: '&?', onBack: '&?', @@ -4041,10 +4369,9 @@ Use this directive to render a group of toggle buttons. controllerAs: 'vm', controller: umbVariantContentEditorsController }; - function umbVariantContentEditorsController($scope, $location, $timeout) { + function umbVariantContentEditorsController($scope, $location, eventsService) { var prevContentDateUpdated = null; var vm = this; - var activeAppAlias = null; vm.$onInit = onInit; vm.$onChanges = onChanges; vm.$doCheck = doCheck; @@ -4054,14 +4381,15 @@ Use this directive to render a group of toggle buttons. vm.selectVariant = selectVariant; vm.selectApp = selectApp; vm.selectAppAnchor = selectAppAnchor; + vm.requestSplitView = requestSplitView; + vm.getScope = getScope; + // used by property editors to get a scope that is the root of split view, content apps etc. //Used to track how many content views there are (for split view there will be 2, it could support more in theory) vm.editors = []; - //Used to track the open variants across the split views - vm.openVariants = []; /** Called when the component initializes */ function onInit() { - prevContentDateUpdated = angular.copy(vm.content.updateDate); - setActiveCulture(); + prevContentDateUpdated = Utilities.copy(vm.content.updateDate); + setActiveVariant(); } /** Called when the component has linked all elements, this is when the form controller is available */ function postLink() { @@ -4072,14 +4400,16 @@ Use this directive to render a group of toggle buttons. */ function onChanges(changes) { if (changes.culture && !changes.culture.isFirstChange() && changes.culture.currentValue !== changes.culture.previousValue) { - setActiveCulture(); + setActiveVariant(); + } else if (changes.segment && !changes.segment.isFirstChange() && changes.segment.currentValue !== changes.segment.previousValue) { + setActiveVariant(); } } /** Allows us to deep watch whatever we want - executes on every digest cycle */ function doCheck() { - if (!angular.equals(vm.content.updateDate, prevContentDateUpdated)) { - setActiveCulture(); - prevContentDateUpdated = angular.copy(vm.content.updateDate); + if (!Utilities.equals(vm.content.updateDate, prevContentDateUpdated)) { + setActiveVariant(); + prevContentDateUpdated = Utilities.copy(vm.content.updateDate); } } /** This is called when the split view changes based on the umb-variant-content */ @@ -4088,36 +4418,35 @@ Use this directive to render a group of toggle buttons. $scope.$broadcast('editors.content.splitViewChanged', { editors: vm.editors }); } /** - * Set the active variant based on the current culture (query string) + * Set the active variant based on the current culture or segment (query string) */ - function setActiveCulture() { + function setActiveVariant() { // set the active variant var activeVariant = null; - _.each(vm.content.variants, function (v) { - if (v.language && v.language.culture === vm.culture) { - v.active = true; + vm.content.variants.forEach(function (v) { + if ((vm.culture === 'invariant' || v.language && v.language.culture === vm.culture) && v.segment === vm.segment) { activeVariant = v; - } else { - v.active = false; } }); if (!activeVariant) { // Set the first variant to active if we can't find it. // If the content item is invariant, then only one item exists in the array. - vm.content.variants[0].active = true; activeVariant = vm.content.variants[0]; } - insertVariantEditor(0, initVariant(activeVariant, 0)); + insertVariantEditor(0, activeVariant); if (vm.editors.length > 1) { //now re-sync any other editor content (i.e. if split view is open) for (var s = 1; s < vm.editors.length; s++) { //get the variant from the scope model - var variant = _.find(vm.content.variants, function (v) { - return v.language.culture === vm.editors[s].content.language.culture; + var variant = vm.content.variants.find(function (v) { + return (!v.language || v.language.culture === vm.editors[s].content.language.culture) && v.segment === vm.editors[s].content.segment; }); - vm.editors[s].content = initVariant(variant, s); + vm.editors[s].content = variant; } } + if (vm.content.variants.length > 1) { + eventsService.emit('editors.content.cultureChanged', activeVariant.language); + } } /** * Updates the editors collection for a given index for the specified variant @@ -4125,142 +4454,93 @@ Use this directive to render a group of toggle buttons. * @param {any} variant */ function insertVariantEditor(index, variant) { + if (vm.editors[index]) { + if (vm.editors[index].content === variant) { + // This variant is already the content of the editor in this index. + return; + } + vm.editors[index].content.active = false; + } + variant.active = true; var variantCulture = variant.language ? variant.language.culture : 'invariant'; - //check if the culture at the index is the same, if it's null an editor will be added - var currentCulture = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].culture; - if (currentCulture !== variantCulture) { - //Not the current culture which means we need to modify the array. + var variantSegment = variant.segment; + var currentCulture = index < vm.editors.length ? vm.editors[index].culture : null; + var currentSegment = index < vm.editors.length ? vm.editors[index].segment : null; + // if index not already exists or if the culture or segment isnt identical then we do a replacement. + if (index >= vm.editors.length || currentCulture !== variantCulture || currentSegment !== variantSegment) { + //Not the current culture or segment which means we need to modify the array. //NOTE: It is not good enough to just replace the `content` object at a given index in the array // since that would mean that directives are not re-initialized. vm.editors.splice(index, 1, { + compositeId: variant.compositeId, content: variant, - //used for "track-by" ng-repeat - culture: variantCulture + culture: variantCulture, + segment: variantSegment }); } else { - //replace the editor for the same culture + //replace the content of the editor, since the culture and segment is the same. vm.editors[index].content = variant; } } - function initVariant(variant, editorIndex) { - //The model that is assigned to the editor contains the current content variant along - //with a copy of the contentApps. This is required because each editor renders it's own - //header and content apps section and the content apps contains the view for editing content itself - //and we need to assign a view model to the subView so that it is scoped to the current - //editor so that split views work. - //copy the apps from the main model if not assigned yet to the variant - if (!variant.apps) { - variant.apps = angular.copy(vm.content.apps); - } - //if this is a variant has a culture/language than we need to assign the language drop down info - if (variant.language) { - //if the variant list that defines the header drop down isn't assigned to the variant then assign it now - if (!variant.variants) { - variant.variants = _.map(vm.content.variants, function (v) { - return _.pick(v, 'active', 'language', 'state'); - }); - } else { - //merge the scope variants on top of the header variants collection (handy when needing to refresh) - angular.extend(variant.variants, _.map(vm.content.variants, function (v) { - return _.pick(v, 'active', 'language', 'state'); - })); - } - //ensure the current culture is set as the active one - for (var i = 0; i < variant.variants.length; i++) { - if (variant.variants[i].language.culture === variant.language.culture) { - variant.variants[i].active = true; - } else { - variant.variants[i].active = false; - } - } - // keep track of the open variants across the different split views - // push the first variant then update the variant index based on the editor index - if (vm.openVariants && vm.openVariants.length === 0) { - vm.openVariants.push(variant.language.culture); - } else { - vm.openVariants[editorIndex] = variant.language.culture; - } - } - //then assign the variant to a view model to the content app - var contentApp = _.find(variant.apps, function (a) { - return a.alias === 'umbContent'; - }); - if (contentApp) { - //The view model for the content app is simply the index of the variant being edited - var variantIndex = vm.content.variants.indexOf(variant); - contentApp.viewModel = variantIndex; - } - // make sure the same app it set to active in the new variant - if (activeAppAlias) { - angular.forEach(variant.apps, function (app) { - app.active = false; - if (app.alias === activeAppAlias) { - app.active = true; - } - }); - } - return variant; - } /** * Adds a new editor to the editors array to show content in a split view * @param {any} selectedVariant */ function openSplitView(selectedVariant) { - var selectedCulture = selectedVariant.language.culture; - //Find the whole variant model based on the culture that was chosen - var variant = _.find(vm.content.variants, function (v) { - return v.language.culture === selectedCulture; + // enforce content contentApp in splitview. + var contentApp = vm.content.apps.find(function (app) { + return app.alias === 'umbContent'; }); - insertVariantEditor(vm.editors.length, initVariant(variant, vm.editors.length)); - //only the content app can be selected since no other apps are shown, and because we copy all of these apps - //to the "editors" we need to update this across all editors - for (var e = 0; e < vm.editors.length; e++) { - var editor = vm.editors[e]; - for (var i = 0; i < editor.content.apps.length; i++) { - var app = editor.content.apps[i]; - if (app.alias === 'umbContent') { - app.active = true; - // tell the world that the app has changed (but do it only once) - if (e === 0) { - selectApp(app); - } - } else { - app.active = false; - } - } + if (contentApp) { + selectApp(contentApp); } - // TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular - editor.collapsed = true; - editor.loading = true; - $timeout(function () { - editor.collapsed = false; - editor.loading = false; - splitViewChanged(); - }, 100); + insertVariantEditor(vm.editors.length, selectedVariant); + splitViewChanged(); } + function requestSplitView(args) { + var culture = args.culture; + var segment = args.segment; + var variant = vm.content.variants.find(function (v) { + return (!v.language || v.language.culture === culture) && v.segment === segment; + }); + if (variant != null) { + openSplitView(variant); + } + } + var unbindSplitViewRequest = eventsService.on('editors.content.splitViewRequest', function (_, args) { + return requestSplitView(args); + }); /** Closes the split view */ function closeSplitView(editorIndex) { // TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular var editor = vm.editors[editorIndex]; - editor.loading = true; - editor.collapsed = true; - $timeout(function () { - vm.editors.splice(editorIndex, 1); - //remove variant from open variants - vm.openVariants.splice(editorIndex, 1); - //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) - $location.search('cculture', vm.openVariants[0]); - splitViewChanged(); - }, 400); + vm.editors.splice(editorIndex, 1); + editor.content.active = false; + //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) + var culture = vm.editors[0].content.language ? vm.editors[0].content.language.culture : null; + $location.search({ + 'cculture': culture, + 'csegment': vm.editors[0].content.segment + }); + splitViewChanged(); + unbindSplitViewRequest(); } + // if split view was never closed, the listener is not disposed when changing nodes - this unbinds it + $scope.$on('$destroy', function () { + return unbindSplitViewRequest(); + }); /** * Changes the currently selected variant * @param {any} variant This is the model of the variant/language drop down item in the editor header * @param {any} editorIndex The index of the editor being changed */ function selectVariant(variant, editorIndex) { - // prevent variants already open in a split view to be opened - if (vm.openVariants.indexOf(variant.language.culture) !== -1) { + var variantCulture = variant.language ? variant.language.culture : 'invariant'; + var variantSegment = variant.segment || null; + // Check if we already have this editor open, if so, do nothing. + if (vm.editors.find(function (editor) { + return (!editor.content.language || editor.content.language.culture === variantCulture) && editor.content.segment === variantSegment; + })) { return; } //if the editor index is zero, then update the query string to track the lang selection, otherwise if it's part @@ -4268,22 +4548,10 @@ Use this directive to render a group of toggle buttons. if (editorIndex === 0) { //If we've made it this far, then update the query string. //The editor will respond to this query string changing. - $location.search('cculture', variant.language.culture); + $location.search('cculture', variantCulture).search('csegment', variantSegment); } else { - //Update the 'active' variant for this editor - var editor = vm.editors[editorIndex]; - //set all variant drop down items as inactive for this editor and then set the selected one as active - for (var i = 0; i < editor.content.variants.length; i++) { - editor.content.variants[i].active = false; - } - variant.active = true; - //get the variant content model and initialize the editor with that - var contentVariant = _.find(vm.content.variants, function (v) { - return v.language.culture === variant.language.culture; - }); - editor.content = initVariant(contentVariant, editorIndex); //update the editors collection - insertVariantEditor(editorIndex, contentVariant); + insertVariantEditor(editorIndex, variant); } } /** @@ -4303,12 +4571,9 @@ Use this directive to render a group of toggle buttons. }); } } - $scope.$on('editors.apps.appChanged', function ($event, $args) { - var app = $args.app; - if (app && app.alias) { - activeAppAlias = app.alias; - } - }); + function getScope() { + return $scope; + } } angular.module('umbraco.directives').component('umbVariantContentEditors', umbVariantContentEditors); }()); @@ -4341,6 +4606,228 @@ Use this directive to render a group of toggle buttons. angular.module('umbraco.directives').component('umbVariantState', umbVariantStateComponent); }()); 'use strict'; + (function () { + 'use strict'; + /** + * A component to render the content type group + */ + function umbContentTypeGroupController() { + var vm = this; + vm.updateName = updateName; + vm.removeGroup = removeGroup; + vm.whenNameFocus = whenNameFocus; + vm.whenFocus = whenFocus; + vm.changeSortOrderValue = changeSortOrderValue; + vm.clickComposition = clickComposition; + function updateName(group) { + if (vm.onUpdateName) { + vm.onUpdateName({ group: group }); + } + } + function removeGroup() { + if (vm.onRemove) { + vm.onRemove({ group: vm.group }); + } + } + function whenNameFocus() { + if (vm.onNameFocus) { + vm.onNameFocus(); + } + } + function whenFocus() { + if (vm.onFocus) { + vm.onFocus(); + } + } + function changeSortOrderValue() { + if (vm.onChangeSortOrderValue) { + vm.onChangeSortOrderValue({ group: vm.group }); + } + } + function clickComposition(documentTypeId) { + if (vm.onClickComposition) { + vm.onClickComposition({ documentTypeId: documentTypeId }); + } + } + } + var umbContentTypeGroupComponent = { + template: '
{{groupNameForm.groupName.errorMsg}}
: {{ vm.group.inheritedFromName }} ,
', + controllerAs: 'vm', + transclude: true, + bindings: { + group: '<', + allowName: '<', + onUpdateName: '&', + allowRemove: '<', + onRemove: '&', + sorting: '<', + onNameFocus: '&', + onFocus: '&', + onChangeSortOrderValue: '&', + valServerFieldName: '@', + valTabAlias: '@', + onClickComposition: '&?' + }, + controller: umbContentTypeGroupController + }; + angular.module('umbraco.directives').component('umbContentTypeGroup', umbContentTypeGroupComponent); + }()); + 'use strict'; + (function () { + 'use strict'; + /** + * A component to render the content type groups + */ + function umbContentTypeGroupsController() { + var vm = this; + } + var umbContentTypeGroupsComponent = { + template: '
', + controllerAs: 'vm', + transclude: true, + controller: umbContentTypeGroupsController + }; + angular.module('umbraco.directives').component('umbContentTypeGroups', umbContentTypeGroupsComponent); + }()); + 'use strict'; + (function () { + 'use strict'; + /** + * A component to render the content type property + */ + function umbContentTypePropertyController() { + var vm = this; + vm.edit = edit; + vm.remove = remove; + vm.changeSortOrderValue = changeSortOrderValue; + function edit() { + if (vm.onEdit) { + vm.onEdit(); + } + } + function remove() { + if (vm.onRemove) { + vm.onRemove({ property: vm.property }); + } + } + function changeSortOrderValue() { + if (vm.onChangeSortOrderValue) { + vm.onChangeSortOrderValue({ property: vm.property }); + } + } + } + var umbContentTypePropertyComponent = { + template: '
{{ vm.property.alias }}
{{propertyTypeForm.groupName.errorMsg}}
{{ vm.property.label }} ({{ vm.property.alias }})
{{vm.property.dataTypeName}}
*
{{vm.property.contentTypeName}}
', + bindings: { + property: '<', + sortable: '<', + onEdit: '&', + onRemove: '&', + onChangeSortOrderValue: '&', + valServerFieldAlias: '@', + valServerFieldLabel: '@', + valTabAlias: '@' + }, + controllerAs: 'vm', + controller: umbContentTypePropertyController + }; + angular.module('umbraco.directives').component('umbContentTypeProperty', umbContentTypePropertyComponent); + }()); + 'use strict'; + (function () { + 'use strict'; + /** + * A component to render the content type tab + */ + function umbContentTypeTabController($timeout) { + var vm = this; + vm.compositionLabelIsVisible = false; + vm.click = click; + vm.removeTab = removeTab; + vm.whenFocusName = whenFocusName; + vm.whenFocus = whenFocus; + vm.changeSortOrderValue = changeSortOrderValue; + vm.changeName = changeName; + vm.clickComposition = clickComposition; + vm.mouseenter = mouseenter; + vm.mouseleave = mouseleave; + var timeout = null; + function click() { + if (vm.onClick) { + vm.onClick({ tab: vm.tab }); + } + } + function removeTab() { + if (vm.onRemove) { + vm.onRemove({ tab: vm.tab }); + } + } + function whenFocusName() { + if (vm.onFocusName) { + vm.onFocusName(); + } + } + function whenFocus() { + if (vm.onFocus) { + vm.onFocus(); + } + } + function changeSortOrderValue() { + if (vm.onChangeSortOrderValue) { + vm.onChangeSortOrderValue({ tab: vm.tab }); + } + } + function changeName() { + if (vm.onChangeName) { + vm.onChangeName({ + key: vm.tab.key, + name: vm.tab.name + }); + } + } + function clickComposition(documentTypeId) { + if (vm.onClickComposition) { + vm.onClickComposition({ documentTypeId: documentTypeId }); + } + } + function mouseenter() { + if (vm.tab.inherited) { + vm.compositionLabelIsVisible = true; + $timeout.cancel(timeout); + } + } + function mouseleave() { + if (vm.tab.inherited) { + timeout = $timeout(function () { + vm.compositionLabelIsVisible = false; + }, 300); + } + } + } + var umbContentTypeTabComponent = { + template: '
: {{ vm.tab.inheritedFromName }} ,
{{ vm.tab.name }}
{{tabNameForm.tabName.errorMsg}}
!
', + controllerAs: 'vm', + transclude: true, + bindings: { + tab: '<', + onClick: '&?', + onClickComposition: '&?', + isOpen: ' 0" ancestors="vm.ancestors" - entity-type="content"> + entity-type="content" + on-open="clickBreadcrumb(ancestor)"> @@ -4617,6 +5105,9 @@ Use this directive to generate a list of breadcrumbs. vm.ancestors = ancestors; }); + $scope.clickBreadcrumb = function(ancestor) { + // manipulate breadcrumb display + } } angular.module("umbraco").controller("My.Controller", Controller); @@ -4625,7 +5116,7 @@ Use this directive to generate a list of breadcrumbs. @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. +@param {callback=} onOpen Function callback when an ancestor is clicked. This will override the default link behaviour. **/ (function () { 'use strict'; @@ -4649,7 +5140,10 @@ Use this directive to generate a list of breadcrumbs. event.preventDefault(); var path = scope.pathTo(ancestor); $location.path(path); - navigationService.clearSearch(['cculture']); + navigationService.clearSearch([ + 'cculture', + 'csegment' + ]); }; scope.pathTo = function (ancestor) { return '/' + scope.entityType + '/' + scope.entityType + '/edit/' + ancestor.id; @@ -4756,8 +5250,8 @@ Use this directive to construct a main content area inside the main editor windo 'use strict'; (function () { 'use strict'; - function EditorContentHeader(serverValidationManager, localizationService, editorState) { - function link(scope, el, attr, ctrl) { + function EditorContentHeader(serverValidationManager, localizationService, editorState, contentEditingHelper) { + function link(scope) { var unsubscribe = []; if (!scope.serverValidationNameField) { scope.serverValidationNameField = 'Name'; @@ -4765,18 +5259,18 @@ Use this directive to construct a main content area inside the main editor windo if (!scope.serverValidationAliasField) { scope.serverValidationAliasField = 'Alias'; } - scope.isNew = scope.content.state == 'NotCreated'; + scope.isNew = scope.editor.content.state == 'NotCreated'; localizationService.localizeMany([ - scope.isNew ? 'visuallyHiddenTexts_createItem' : 'visuallyHiddenTexts_edit', - 'visuallyHiddenTexts_name', + scope.isNew ? 'placeholders_a11yCreateItem' : 'placeholders_a11yEdit', + 'placeholders_a11yName', scope.isNew ? 'general_new' : 'general_edit' ]).then(function (data) { scope.a11yMessage = data[0]; scope.a11yName = data[1]; var title = data[2] + ': '; if (!scope.isNew) { - scope.a11yMessage += ' ' + scope.content.name; - title += scope.content.name; + scope.a11yMessage += ' ' + scope.editor.content.name; + title += scope.editor.content.name; } else { var name = editorState.current.contentTypeName; scope.a11yMessage += ' ' + name; @@ -4786,59 +5280,119 @@ Use this directive to construct a main content area inside the main editor windo scope.$emit('$changeTitle', title); }); scope.vm = {}; + scope.vm.hasVariants = false; + scope.vm.hasSubVariants = false; + scope.vm.hasCulture = false; + scope.vm.hasSegments = false; scope.vm.dropdownOpen = false; - scope.vm.currentVariant = ''; scope.vm.variantsWithError = []; scope.vm.defaultVariant = null; scope.vm.errorsOnOtherVariants = false; // indicating wether to show that other variants, than the current, have errors. + function updateVaraintErrors() { + scope.content.variants.forEach(function (variant) { + variant.hasError = scope.variantHasError(variant); + }); + checkErrorsOnOtherVariants(); + } function checkErrorsOnOtherVariants() { var check = false; - angular.forEach(scope.content.variants, function (variant) { - if (scope.openVariants.indexOf(variant.language.culture) === -1 && scope.variantHasError(variant.language.culture)) { + scope.content.variants.forEach(function (variant) { + if (variant.active !== true && variant.hasError) { check = true; } }); scope.vm.errorsOnOtherVariants = check; } - function onCultureValidation(valid, errors, allErrors, culture) { - var index = scope.vm.variantsWithError.indexOf(culture); + function onVariantValidation(valid, errors, allErrors, culture, segment) { + // only want to react to property errors: + if (errors.findIndex(function (error) { + return error.propertyAlias !== null; + }) === -1) { + // we dont have any errors for properties, meaning we will back out. + return; + } + // If error coming back is invariant, we will assign the error to the default variant by picking the defaultVariant language. + if (culture === 'invariant' && scope.vm.defaultVariant) { + culture = scope.vm.defaultVariant.language.culture; + } + var index = scope.vm.variantsWithError.findIndex(function (item) { + return item.culture === culture && item.segment === segment; + }); if (valid === true) { if (index !== -1) { scope.vm.variantsWithError.splice(index, 1); } } else { if (index === -1) { - scope.vm.variantsWithError.push(culture); + scope.vm.variantsWithError.push({ + 'culture': culture, + 'segment': segment + }); } } - checkErrorsOnOtherVariants(); + scope.$evalAsync(updateVaraintErrors); } function onInit() { - // find default. - angular.forEach(scope.content.variants, function (variant) { - if (variant.language.isDefault) { + // find default + check if we have variants. + scope.content.variants.forEach(function (variant) { + if (variant.language !== null && variant.language.isDefault) { scope.vm.defaultVariant = variant; } + if (variant.language !== null) { + scope.vm.hasCulture = true; + } + if (variant.segment !== null) { + scope.vm.hasSegments = true; + } }); - setCurrentVariant(); - angular.forEach(scope.content.apps, function (app) { + scope.vm.hasVariants = scope.content.variants.length > 1 && (scope.vm.hasCulture || scope.vm.hasSegments); + scope.vm.hasSubVariants = scope.content.variants.length > 1 && scope.vm.hasCulture && scope.vm.hasSegments; + updateVaraintErrors(); + scope.vm.variantMenu = []; + if (scope.vm.hasCulture) { + scope.content.variants.forEach(function (v) { + if (v.language !== null && v.segment === null) { + var subVariants = scope.content.variants.filter(function (subVariant) { + return subVariant.language.culture === v.language.culture && subVariant.segment !== null; + }).sort(contentEditingHelper.sortVariants); + var variantMenuEntry = { + key: String.CreateGuid(), + open: v.language && v.language.culture === scope.editor.culture, + variant: v, + subVariants: subVariants + }; + scope.vm.variantMenu.push(variantMenuEntry); + } + }); + } else { + scope.content.variants.forEach(function (v) { + scope.vm.variantMenu.push({ + key: String.CreateGuid(), + variant: v + }); + }); + } + scope.editor.variantApps.forEach(function (app) { + // only render quick links on the content app if there are no tabs if (app.alias === 'umbContent') { - app.anchors = scope.content.tabs; + var hasTabs = scope.editor.content.tabs && scope.editor.content.tabs.filter(function (group) { + return group.type === 1; + }).length > 0; + app.anchors = hasTabs ? [] : scope.editor.content.tabs; } }); - angular.forEach(scope.content.variants, function (variant) { - unsubscribe.push(serverValidationManager.subscribe(null, variant.language.culture, null, onCultureValidation)); - }); - unsubscribe.push(serverValidationManager.subscribe(null, null, null, onCultureValidation)); - } - function setCurrentVariant() { - angular.forEach(scope.content.variants, function (variant) { - if (variant.active) { - scope.vm.currentVariant = variant; - checkErrorsOnOtherVariants(); + scope.content.variants.forEach(function (variant) { + // if we are looking for the variant with default language then we also want to check for invariant variant. + if (variant.language && scope.vm.defaultVariant && variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) { + unsubscribe.push(serverValidationManager.subscribe(null, 'invariant', null, onVariantValidation, null)); } + unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, null, onVariantValidation, variant.segment)); }); + scope.vm.variantMenu.sort(sortVariantsMenu); + } + function sortVariantsMenu(a, b) { + return contentEditingHelper.sortVariants(a.variant, b.variant); } scope.goBack = function () { if (scope.onBack) { @@ -4876,45 +5430,27 @@ Use this directive to construct a main content area inside the main editor windo } }; /** - * keep track of open variants - this is used to prevent the same variant to be open in more than one split view - * @param {any} culture - */ - scope.variantIsOpen = function (culture) { - return scope.openVariants.indexOf(culture) !== -1; - }; - /** * Check whether a variant has a error, used to display errors in variant switcher. * @param {any} culture */ - scope.variantHasError = function (culture) { - // if we are looking for the default language we also want to check for invariant. - if (culture === scope.vm.defaultVariant.language.culture) { - if (scope.vm.variantsWithError.indexOf('invariant') !== -1) { - return true; - } - } - if (scope.vm.variantsWithError.indexOf(culture) !== -1) { + scope.variantHasError = function (variant) { + if (scope.vm.variantsWithError.find(function (item) { + return (!variant.language || item.culture === variant.language.culture) && item.segment === variant.segment; + }) !== undefined) { return true; } return false; }; + scope.toggleDropdown = function () { + scope.vm.dropdownOpen = !scope.vm.dropdownOpen; + if (scope.vm.dropdownOpen) { + scope.vm.variantMenu.sort(sortVariantsMenu); + } + }; + unsubscribe.push(scope.$watch('splitViewOpen', function (newVal) { + scope.vm.navigationItemLimit = newVal === true ? 0 : undefined; + })); onInit(); - //watch for the active culture changing, if it changes, update the current variant - if (scope.content.variants) { - scope.$watch(function () { - for (var i = 0; i < scope.content.variants.length; i++) { - var v = scope.content.variants[i]; - if (v.active) { - return v.language.culture; - } - } - return scope.vm.currentVariant.language.culture; //should never get here - }, function (newValue, oldValue) { - if (newValue !== scope.vm.currentVariant.language.culture) { - setCurrentVariant(); - } - }); - } scope.$on('$destroy', function () { for (var u in unsubscribe) { unsubscribe[u](); @@ -4925,14 +5461,15 @@ Use this directive to construct a main content area inside the main editor windo transclude: true, restrict: 'E', replace: true, - template: '

{{a11yMessage}}

{{vm.currentVariant.language.name}}
Open in split view
', + template: '

{{a11yMessage}}

Open in split view
Open in split view
', scope: { name: '=', nameDisabled: ' @param {string} name The content name. +@param {boolean=} nameRequired Require name to be defined. (True by default) @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. @@ -5326,13 +5864,14 @@ Use this directive to construct a header inside the main editor window. @param {boolean=} aliasLocked Set to true to lock the alias. @param {boolean=} hideAlias Set to true to hide alias. @param {string=} description Add a description to the content. +@param {boolean=} descriptionLocked Set to true to lock the description. @param {boolean=} hideDescription Set to true to hide description. -@param {boolean=} setpagetitle If true the page title will be set to reflect the type of data the header is working with +@param {boolean=} setpagetitle If true the page title will be set to reflect the type of data the header is working with @param {string=} editorfor The localization to use to aid accessibility on the edit and create screen **/ (function () { 'use strict'; - function EditorHeaderDirective(editorService, localizationService, editorState) { + function EditorHeaderDirective(editorService, localizationService, editorState, $rootScope) { function link(scope, $injector) { scope.vm = {}; scope.vm.dropdownOpen = false; @@ -5347,21 +5886,12 @@ Use this directive to construct a header inside the main editor window. if (editorState.current) { //to do make work for user create/edit // to do make it work for user group create/ edit - // to do make it work for language edit/create - // to do make it work for log viewer - scope.isNew = editorState.current.id === 0 || editorState.current.id === '0' || editorState.current.id === -1 || editorState.current.id === 0 || editorState.current.id === '-1'; - var localizeVars = [ - scope.isNew ? 'visuallyHiddenTexts_createItem' : 'visuallyHiddenTexts_edit', - 'visuallyHiddenTexts_name', - scope.isNew ? 'general_new' : 'general_edit' - ]; - if (scope.editorfor) { - localizeVars.push(scope.editorfor); - } - localizationService.localizeMany(localizeVars).then(function (data) { - setAccessibilityForEditor(data); - scope.loading = false; - }); + // to make it work for language edit/create + setAccessibilityForEditorState(); + scope.loading = false; + } else if (scope.name) { + setAccessibilityForName(); + scope.loading = false; } else { scope.loading = false; } @@ -5397,23 +5927,54 @@ Use this directive to construct a header inside the main editor window. }; editorService.iconPicker(iconPicker); }; - function setAccessibilityForEditor(data) { - if (editorState.current) { - if (scope.nameLocked) { - scope.accessibility.a11yName = scope.name; - SetPageTitle(scope.name); + function setAccessibilityForName() { + var setTitle = false; + if (scope.setpagetitle !== undefined) { + setTitle = scope.setpagetitle; + } + if (setTitle) { + setAccessibilityHeaderDirective(false, scope.editorfor, scope.nameLocked, scope.name, '', true); + } + } + function setAccessibilityForEditorState() { + var isNew = editorState.current.id === 0 || editorState.current.id === '0' || editorState.current.id === -1 || editorState.current.id === 0 || editorState.current.id === '-1'; + var contentTypeName = ''; + if (editorState.current.contentTypeName) { + contentTypeName = editorState.current.contentTypeName; + } + var setTitle = false; + if (scope.setpagetitle !== undefined) { + setTitle = scope.setpagetitle; + } + setAccessibilityHeaderDirective(isNew, scope.editorfor, scope.nameLocked, scope.name, contentTypeName, setTitle); + } + function setAccessibilityHeaderDirective(isNew, editorFor, nameLocked, entityName, contentTypeName, setTitle) { + var localizeVars = [ + isNew ? 'visuallyHiddenTexts_createItem' : 'visuallyHiddenTexts_edit', + 'visuallyHiddenTexts_name', + isNew ? 'general_new' : 'general_edit' + ]; + if (editorFor) { + localizeVars.push(editorFor); + } + localizationService.localizeMany(localizeVars).then(function (data) { + if (nameLocked) { + scope.accessibility.a11yName = entityName; + if (setTitle) { + SetPageTitle(entityName); + } } else { scope.accessibility.a11yMessage = data[0]; scope.accessibility.a11yName = data[1]; var title = data[2] + ':'; - if (!scope.isNew) { - scope.accessibility.a11yMessage += ' ' + scope.name; - title += ' ' + scope.name; + if (!isNew) { + scope.accessibility.a11yMessage += ' ' + entityName; + title += ' ' + entityName; } else { var name = ''; - if (editorState.current.contentTypeName) { + if (contentTypeName) { name = editorState.current.contentTypeName; - } else if (scope.editorfor) { + } else if (editorFor) { name = data[3]; } if (name !== '') { @@ -5422,35 +5983,36 @@ Use this directive to construct a header inside the main editor window. title += ' ' + name; } } - if (title !== data[2] + ':') { + if (setTitle && title !== data[2] + ':') { SetPageTitle(title); } } - scope.accessibility.a11yMessageVisible = !isEmptyOrSpaces(scope.accessibility.a11yMessage); - scope.accessibility.a11yNameVisible = !isEmptyOrSpaces(scope.accessibility.a11yName); - } + scope.accessibility.a11yMessageVisible = !isNullOrWhitespace(scope.accessibility.a11yMessage); + scope.accessibility.a11yNameVisible = !isNullOrWhitespace(scope.accessibility.a11yName); + }); } - function isEmptyOrSpaces(str) { - return str === null || str === undefined || str.trim === ''; + function isNullOrWhitespace(str) { + return str === null || str === undefined || str.trim() === ''; } function SetPageTitle(title) { - var setTitle = false; - if (scope.setpagetitle !== undefined) { - setTitle = scope.setpagetitle; - } - if (setTitle) { - scope.$emit('$changeTitle', title); - } + scope.$emit('$changeTitle', title); } + var unbindEventHandler = $rootScope.$on('$setAccessibleHeader', function (event, isNew, editorFor, nameLocked, name, contentTypeName, setTitle) { + setAccessibilityHeaderDirective(isNew, editorFor, nameLocked, name, contentTypeName, setTitle); + }); + scope.$on('$destroy', function () { + unbindEventHandler(); + }); } var directive = { transclude: true, restrict: 'E', replace: true, - template: '

{{accessibility.a11yMessage}}

{{ name }}
{{ description }}
', + template: '

{{accessibility.a11yMessage}}

{{ name }}

{{ description }}

', scope: { name: '=', nameLocked: '=', + nameRequired: '=?', menu: '=', hideActionsMenu: ' ', + template: '
', link: link, scope: { currentNode: '=', - currentSection: '@' + currentSection: '@', + isDisabled: ' 1; + scope.showNavigation = newLength > 1; calculateVisibleItems($window.innerWidth); } - }); + setMoreButtonErrorState(); + }, true)); } function calculateVisibleItems(windowWidth) { // if we don't get a windowWidth stick with the default item limit if (!windowWidth) { return; } - scope.itemsLimit = 0; - // set visible items based on browser width - if (windowWidth > 1500) { - scope.itemsLimit = 6; - } else if (windowWidth > 700) { - scope.itemsLimit = 4; + // if we haven't set a specific limit prop we base the amount of visible items on the window width + if (scope.limit === undefined) { + scope.itemsLimit = 0; + // set visible items based on browser width + if (windowWidth > 1500) { + scope.itemsLimit = 6; + } else if (windowWidth > 700) { + scope.itemsLimit = 4; + } } // toggle more button if (scope.navigation.length > scope.itemsLimit) { @@ -5593,6 +6165,9 @@ Use this directive to construct a header inside the main editor window. scope.showMoreButton = false; scope.overflowingItems = 0; } + scope.moreButton.name = scope.itemsLimit === 0 ? 'Menu' : 'More'; + setMoreButtonActiveState(); + setMoreButtonErrorState(); } function runItemAction(selectedItem) { if (selectedItem.action) { @@ -5602,40 +6177,66 @@ Use this directive to construct a header inside the main editor window. function setItemToActive(selectedItem) { if (selectedItem.view) { // deselect all items - angular.forEach(scope.navigation, function (item, index) { + Utilities.forEach(scope.navigation, function (item) { item.active = false; }); // set clicked item to active selectedItem.active = true; - // set more button to active if item in dropdown is clicked - var selectedItemIndex = scope.navigation.indexOf(selectedItem); - if (selectedItemIndex + 1 > scope.itemsLimit) { - scope.moreButton.active = true; - } else { - scope.moreButton.active = false; - } + setMoreButtonActiveState(); + setMoreButtonErrorState(); + } + } + function setMoreButtonActiveState() { + // set active state on more button if any of the overflown items is active + scope.moreButton.active = scope.navigation.findIndex(function (item) { + return item.active; + }) + 1 > scope.itemsLimit; + } + ; + function setMoreButtonErrorState() { + if (scope.overflowingItems === 0) { + return; } + var overflow = scope.navigation.slice(scope.itemsLimit, scope.navigation.length); + var active = scope.navigation.find(function (item) { + return item.active; + }); + // set error state on more button if any of the overflown items has an error. We use it show the error badge and color the item + scope.moreButton.hasError = overflow.filter(function (item) { + return item.hasError; + }).length > 0; + // set special active/error state on button if the current selected item is has an error + // we don't want to show the error badge in this case so we need a special state for that + scope.moreButton.activeHasError = active.hasError; } + ; var resizeCallback = function resizeCallback(size) { if (size && size.width) { calculateVisibleItems(size.width); } }; windowResizeListener.register(resizeCallback); - //ensure to unregister from all events and kill jquery plugins + unsubscribe.push(scope.$watch('limit', function (newVal) { + scope.itemsLimit = newVal; + calculateVisibleItems($window.innerWidth); + })); scope.$on('$destroy', function () { windowResizeListener.unregister(resizeCallback); - }); + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); onInit(); } var directive = { restrict: 'E', replace: true, - template: ' ', + template: '
  • !
', scope: { navigation: '=', onSelect: '&', - onAnchorSelect: '&' + onAnchorSelect: '&', + limit: '<' }, link: link }; @@ -5648,7 +6249,11 @@ Use this directive to construct a header inside the main editor window. 'use strict'; function UmbEditorNavigationItemController($scope, $element, $attrs) { var vm = this; + vm.close = function () { + vm.expanded = false; + }; vm.clicked = function () { + vm.expanded = vm.item.anchors && vm.item.anchors.length > 1 && !vm.expanded; vm.onOpen({ item: vm.item }); }; vm.anchorClicked = function (anchor, $event) { @@ -5672,7 +6277,7 @@ Use this directive to construct a header inside the main editor window. }); } angular.module('umbraco.directives.html').component('umbEditorNavigationItem', { - template: ' ', + template: ' Jump to {{anchor.label}} group ', controller: UmbEditorNavigationItemController, controllerAs: 'vm', bindings: { @@ -5686,11 +6291,18 @@ Use this directive to construct a header inside the main editor window. 'use strict'; (function () { 'use strict'; - function EditorsDirective($timeout, eventsService) { + function EditorsDirective($timeout, eventsService, focusLockService) { function link(scope, el, attr, ctrl) { var evts = []; var allowedNumberOfVisibleEditors = 3; + var aboveBackDropCssClass = 'above-backdrop'; + var sectionId = '#leftcolumn'; + var isLeftColumnAbove = false; scope.editors = []; + /* we need to keep a count of open editors because the length of the editors array is first changed when animations are done + we do this because some infinite editors close more than one editor at the time and we get the wrong count from editors.length + because of the animation */ + var editorCount = 0; function addEditor(editor) { editor.inFront = true; editor.moveRight = true; @@ -5698,6 +6310,14 @@ Use this directive to construct a header inside the main editor window. editor.styleIndex = 0; // push the new editor to the dom scope.editors.push(editor); + if (scope.editors.length === 1) { + isLeftColumnAbove = $(sectionId).hasClass(aboveBackDropCssClass); + if (isLeftColumnAbove) { + $(sectionId).removeClass(aboveBackDropCssClass); + } + // Inert content in the #mainwrapper + focusLockService.addInertAttribute(); + } $timeout(function () { editor.moveRight = false; }); @@ -5710,6 +6330,17 @@ Use this directive to construct a header inside the main editor window. editor.animating = true; setTimeout(removeEditorFromDOM.bind(this, editor), 400); updateEditors(-1); + if (scope.editors.length === 1) { + if (isLeftColumnAbove) { + $('#leftcolumn').addClass(aboveBackDropCssClass); + } + isLeftColumnAbove = false; + } + // when the last editor is closed remove the focus lock + if (editorCount === 0) { + // Remove the inert attribute from the #mainwrapper + focusLockService.removeInertAttribute(); + } } function revealEditorContent(editor) { editor.animating = false; @@ -5742,16 +6373,21 @@ Use this directive to construct a header inside the main editor window. } } evts.push(eventsService.on('appState.editors.open', function (name, args) { + editorCount = editorCount + 1; addEditor(args.editor); })); evts.push(eventsService.on('appState.editors.close', function (name, args) { // remove the closed editor if (args && args.editor) { + editorCount = editorCount - 1; removeEditor(args.editor); } // close all editors if (args && !args.editor && args.editors.length === 0) { + editorCount = 0; scope.editors = []; + // Remove the inert attribute from the #mainwrapper + focusLockService.removeInertAttribute(); } })); //ensure to unregister from all events! @@ -5764,12 +6400,64 @@ Use this directive to construct a header inside the main editor window. var directive = { restrict: 'E', replace: true, - template: '
', + template: '
', + link: link + }; + return directive; + } + // This directive allows for us to run a custom $compile for the view within the repeater which allows + // us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the + // infinite editing. The retain the $scope hiearchy a special $parentScope property is passed in to the model. + function EditorRepeaterDirective($http, $templateCache, $compile, angularHelper) { + function link(scope, el) { + var editor = scope && scope.$parent ? scope.$parent.model : null; + if (!editor) { + return; + } + var unsubscribe = []; + //if a custom parent scope is defined then we need to manually compile the view + if (editor.$parentScope) { + var element = el.find('.scoped-view'); + $http.get(editor.view, { cache: $templateCache }).then(function (response) { + var templateScope = editor.$parentScope.$new(); + unsubscribe.push(function () { + templateScope.$destroy(); + }); + // NOTE: the 'model' name here directly affects the naming convention used in infinite editors, this why you access the model + // like $scope.model.If this is changed, everything breaks.This is because we are entirely reliant upon ng-include and inheriting $scopes. + // by default without a $parentScope used for infinite editing the 'model' propety will be set because the view creates the scopes in + // ng-repeat by ng-repeat="model in editors" + templateScope.model = editor; + element.show(); + // if a parentForm is supplied then we can link them but to do that we need to inject a top level form + if (editor.$parentForm) { + element.html('' + response.data + ''); + } + $compile(element)(templateScope); + // if a parentForm is supplied then we can link them + if (editor.$parentForm) { + editor.$parentForm.$addControl(templateScope.infiniteEditorForm); + } + }); + } + scope.$on('$destroy', function () { + for (var i = 0; i < unsubscribe.length; i++) { + unsubscribe[i](); + } + }); + } + var directive = { + restrict: 'E', + replace: true, + transclude: true, + scope: { editors: '=' }, + template: '
', link: link }; return directive; } angular.module('umbraco.directives').directive('umbEditors', EditorsDirective); + angular.module('umbraco.directives').directive('umbEditorRepeater', EditorRepeaterDirective); }()); 'use strict'; (function () { @@ -5778,7 +6466,7 @@ Use this directive to construct a header inside the main editor window. * A directive that renders a defined view with a view model and a the whole content model. **/ function EditorSubViewDirective() { - function link(scope, el, attr, ctrl) { + function link(scope) { //The model can contain: view, viewModel, name, alias, icon if (!scope.model.view) { throw 'No view defined for the content app'; @@ -5787,9 +6475,10 @@ Use this directive to construct a header inside the main editor window. var directive = { restrict: 'E', replace: true, - template: '
', + template: '
', scope: { model: '=', + variantContent: '=?', content: '=' }, link: link @@ -5812,7 +6501,7 @@ Use this directive to construct a header inside the main editor window. var directive = { restrict: 'E', replace: true, - template: '
', + template: '
', scope: { subViews: '=', model: '=' @@ -5824,6 +6513,23 @@ Use this directive to construct a header inside the main editor window. angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective); }()); 'use strict'; + (function () { + 'use strict'; + /** + * A component to render the editor tab bar + */ + function umbEditorTabBarController() { + var vm = this; + } + var umbEditorTabBarComponent = { + template: '
', + controllerAs: 'vm', + transclude: true, + controller: umbEditorTabBarController + }; + angular.module('umbraco.directives').component('umbEditorTabBar', umbEditorTabBarComponent); + }()); + 'use strict'; /** @ngdoc directive @name umbraco.directives.directive:umbEditorView @@ -5885,6 +6591,8 @@ Use this directive to construct the main editor window.
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • + +@param {boolean} footer Whether the directive should make place for a {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter} at the bottom (`true` by default). **/ (function () { 'use strict'; @@ -6144,7 +6852,7 @@ Use this directive to construct the main editor window. return; } // please to not use angularHelper.safeApply here, it won't work - scope.$apply(attrs.onOutsideClick); + scope.$evalAsync(attrs.onOutsideClick); } $timeout(function () { if ('bindClickOn' in attrs) { @@ -6211,20 +6919,20 @@ Use this directive to construct the main editor window. function ($parse, $compile) { // contains function contains(arr, item) { - if (angular.isArray(arr)) { + if (Utilities.isArray(arr)) { for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { + if (Utilities.equals(arr[i], item)) { return true; } } } return false; } - // add + // add function add(arr, item) { - arr = angular.isArray(arr) ? arr : []; + arr = Utilities.isArray(arr) ? arr : []; for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { + if (Utilities.equals(arr[i], item)) { return arr; } } @@ -6233,9 +6941,9 @@ Use this directive to construct the main editor window. } // remove function remove(arr, item) { - if (angular.isArray(arr)) { + if (Utilities.isArray(arr)) { for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { + if (Utilities.equals(arr[i], item)) { arr.splice(i, 1); break; } @@ -6344,7 +7052,7 @@ Use this directive to construct the main editor window. }); //always try to format the model value as an int ctrl.$formatters.push(function (value) { - if (angular.isString(value)) { + if (Utilities.isString(value)) { return parseFloat(value, 10); } return value; @@ -6371,13 +7079,17 @@ Use this directive to construct the main editor window. return { restrict: 'A', link: function link(scope, elm, attrs, ctrl) { + var delayTimer; attrs.$observe('focusWhen', function (newValue) { - if (newValue === 'true') { - $timeout(function () { - elm.trigger('focus'); + if (newValue === 'true' && document.activeelement !== elm[0]) { + delayTimer = $timeout(function () { + elm[0].focus(); }); } }); + scope.$on('$destroy', function () { + $timeout.cancel(delayTimer); + }); } }; }); @@ -6394,8 +7106,7 @@ Use this directive to construct the main editor window. restrict: 'A', link: function link(scope, element, attr, formCtrl) { function setBackgroundColor(color) { - // note: can't use element.css(), it doesn't support hexa background colors - angular.element(element)[0].style.backgroundColor = '#' + color; + element[0].style.backgroundColor = '#' + color; } // Only add inline hex background color if defined and not "true". if (attr.hexBgInline === undefined || attr.hexBgInline !== undefined && attr.hexBgInline === 'true') { @@ -6534,7 +7245,7 @@ Use this directive to prevent default action of an element. Effectively implemen }); } $(element).on('keypress', function (event) { - if (event.which === 13) { + if (event.which === 13 && enabled === true) { event.preventDefault(); } }); @@ -6589,18 +7300,21 @@ Use this directive to prevent default action of an element. Effectively implemen }); 'use strict'; angular.module('umbraco.directives').directive('umbAutoFocus', function ($timeout) { - return function (scope, element, attr) { + return function (scope, element, attrs) { var update = function update() { //if it uses its default naming - if (element.val() === '' || attr.focusOnFilled) { + if (element.val() === '' || attrs.focusOnFilled) { element.trigger('focus'); } }; - if (attr.umbAutoFocus !== 'false') { - $timeout(function () { - update(); - }); - } + attrs.$observe('umbAutoFocus', function (newVal) { + var enabled = newVal === 'false' || newVal === 0 || newVal === false ? false : true; + if (enabled) { + $timeout(function () { + update(); + }); + } + }); }; }); 'use strict'; @@ -6615,66 +7329,17 @@ Use this directive to prevent default action of an element. Effectively implemen 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 the element is hidden the width will be 0 even though it has a value. + // This could happen if the element is hidden in a tab. + if (ngModelController.$modelValue && domEl.clientWidth === 0) { + element.width('auto'); + } if (!ngModelController.$modelValue && attr.placeholder) { attr.$set('size', attr.placeholder.length); element.width('auto'); @@ -6693,17 +7358,12 @@ Use this directive to prevent default action of an element. Effectively implemen element.width(0); } } - if (isIEFlag === true && domElType === 'text') { - resizeInternetExplorerInput(); - } else { - if (domElType === 'textarea') { - resizeTextarea(); - } else if (domElType === 'text') { - resizeInput(); - } + if (domElType === 'textarea') { + resizeTextarea(); + } else if (domElType === 'text') { + resizeInput(); } }; - activate(); //listen for tab changes if (umbTabsController != null) { umbTabsController.onTabShown(function (args) { @@ -6722,10 +7382,6 @@ Use this directive to prevent default action of an element. Effectively implemen element.off('keyup keydown keypress change', update); element.off('blur', update(true)); unbindModelWatcher(); - // clean up IE dom element - if (isIEFlag === true && domElType === 'text') { - mirror.remove(); - } }); } }; @@ -6764,17 +7420,22 @@ Use this directive to prevent default action of an element. Effectively implemen @param {boolean} disabled Set the checkbox to be disabled. @param {boolean} required Set the checkbox to be required. @param {callback} onChange Callback when the value of the checkbox change by interaction. -@param {string} cssClass Set a css class modifier +@param {string} cssClass Set a css class modifier. +@deprecated @param {string} iconClass Set an icon next to checkbox. Use "icon" parameter instead. +@param {string} icon Set an icon next to checkbox. +@param {boolean} disableDirtyCheck Disable checking if the model is dirty. **/ (function () { 'use strict'; - function UmbCheckboxController($timeout, localizationService) { + function UmbCheckboxController($timeout, $attrs, localizationService) { var vm = this; vm.$onInit = onInit; vm.change = change; function onInit() { vm.inputId = vm.inputId || 'umb-check_' + String.CreateGuid(); + vm.disableDirtyCheck = $attrs.hasOwnProperty('disableDirtyCheck') && vm.disableDirtyCheck !== '0' && vm.disableDirtyCheck !== 0 && vm.disableDirtyCheck !== 'false' && vm.disableDirtyCheck !== false; + vm.icon = vm.icon || vm.iconClass || null; // If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [ if (vm.labelKey) { localizationService.localize(vm.labelKey).then(function (data) { @@ -6796,9 +7457,10 @@ Use this directive to prevent default action of an element. Effectively implemen } } var component = { - template: ' ', + template: ' ', controller: UmbCheckboxController, controllerAs: 'vm', + transclude: true, bindings: { model: '=', inputId: '@', @@ -6811,12 +7473,211 @@ Use this directive to prevent default action of an element. Effectively implemen required: '<', onChange: '&?', cssClass: '@?', - iconClass: '@?' + iconClass: '@?', + // deprecated + icon: '@?', + disableDirtyCheck: '=?' } }; angular.module('umbraco.directives').component('umbCheckbox', component); }()); 'use strict'; + (function () { + 'use strict'; + function FocusLock($timeout, $rootScope, angularHelper) { + // If the umb-auto-focus directive is in use we respect that by leaving the default focus on it instead of choosing the first focusable element using this function + function getAutoFocusElement(elements) { + var elmentWithAutoFocus = null; + elements.forEach(function (element) { + if (element.getAttribute('umb-auto-focus') === 'true') { + elmentWithAutoFocus = element; + } + }); + return elmentWithAutoFocus; + } + function link(scope, element) { + var target = element[0]; + var focusableElements; + var firstFocusableElement; + var lastFocusableElement; + var infiniteEditorsWrapper; + var infiniteEditors; + var disconnectObserver = false; + var closingEditor = false; + if (!$rootScope.lastKnownFocusableElements) { + $rootScope.lastKnownFocusableElements = []; + } + $rootScope.lastKnownFocusableElements.push(document.activeElement); + // List of elements that can be focusable within the focus lock + var focusableElementsSelector = '[role="button"], a[href]:not([disabled]):not(.ng-hide), button:not([disabled]):not(.ng-hide), textarea:not([disabled]):not(.ng-hide), input:not([disabled]):not(.ng-hide), select:not([disabled]):not(.ng-hide)'; + // Grab the body element so we can add the tabbing class on it when needed + var bodyElement = document.querySelector('body'); + function getDomNodes() { + infiniteEditorsWrapper = document.querySelector('.umb-editors'); + if (infiniteEditorsWrapper) { + infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor') || []); + } + } + function getFocusableElements(targetElm) { + var elm = targetElm ? targetElm : target; + focusableElements = elm.querySelectorAll(focusableElementsSelector); + // Set first and last focusable elements + firstFocusableElement = focusableElements[0]; + lastFocusableElement = focusableElements[focusableElements.length - 1]; + } + function handleKeydown(event) { + var isTabPressed = event.key === 'Tab' || event.keyCode === 9; + if (!isTabPressed) { + return; + } + // If shift + tab key + if (event.shiftKey) { + // Set focus on the last focusable element if shift+tab are pressed meaning we go backwards + if (document.activeElement === firstFocusableElement) { + lastFocusableElement.focus(); + event.preventDefault(); + } + } // Else only the tab key is pressed + else { + // Using only the tab key we set focus on the first focusable element mening we go forward + if (document.activeElement === lastFocusableElement) { + firstFocusableElement.focus(); + event.preventDefault(); + } + } + } + function clearLastKnownFocusedElements() { + $rootScope.lastKnownFocusableElements = []; + } + function setElementFocus() { + var defaultFocusedElement = getAutoFocusElement(focusableElements); + var lastKnownElement; + // If an infinite editor is being closed then we reset the focus to the element that triggered the the overlay + if (closingEditor) { + // If there is only one editor open, search for the "editor-info" inside it and set focus on it + // This is relevant when a property editor has been selected and the editor where we selected it from + // is closed taking us back to the first layer + // Otherwise set it to the last element in the lastKnownFocusedElements array + if (infiniteEditors && infiniteEditors.length === 1) { + var editorInfo = infiniteEditors[0].querySelector('.editor-info'); + if (infiniteEditors && infiniteEditors.length === 1 && editorInfo !== null) { + lastKnownElement = editorInfo; + // Clear the array + clearLastKnownFocusedElements(); + } + } else { + var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; + lastKnownElement = $rootScope.lastKnownFocusableElements[lastItemIndex]; + // Remove the last item from the array so we always set the correct lastKnowFocus for each layer + $rootScope.lastKnownFocusableElements.splice(lastItemIndex, 1); + } + // Update the lastknowelement variable here + closingEditor = false; + } + // 1st - we check for any last known element - Usually the element the trigger the opening of a new layer + // If it exists it will receive fous + // 2nd - We check to see if a default focus has been set using the umb-auto-focus directive. If not we set focus on + // the first focusable element + // 3rd - Otherwise put the focus on the default focused element + if (lastKnownElement) { + lastKnownElement.focus(); + } else if (defaultFocusedElement === null) { + firstFocusableElement.focus(); + } else { + defaultFocusedElement.focus(); + } + } + function observeDomChanges() { + // Watch for DOM changes - so we can refresh the focusable elements if an element + // changes from being disabled to being enabled for instance + var observer = new MutationObserver(_.debounce(domChange, 200)); + // Options for the observer (which mutations to observe) + var config = { + attributes: true, + childList: true, + subtree: true + }; + // Whenever the DOM changes ensure the list of focused elements is updated + function domChange() { + getFocusableElements(); + } + // Start observing the target node for configured mutations + observer.observe(target, config); + // Disconnect observer + if (disconnectObserver) { + observer.disconnect(); + } + } + function cleanupEventHandlers() { + //if we're in infinite editing mode + if (infiniteEditors.length > 0) { + var activeEditor = infiniteEditors[infiniteEditors.length - 1]; + var inactiveEditors = infiniteEditors.filter(function (editor) { + return editor !== activeEditor; + }); + if (inactiveEditors.length > 0) { + for (var index = 0; index < inactiveEditors.length; index++) { + var inactiveEditor = inactiveEditors[index]; + // Remove event handlers from inactive editors + inactiveEditor.removeEventListener('keydown', handleKeydown); + } + } else { + // Why is this one only begin called if there is no other infinite editors, wouldn't it make sense always to clean this up? + // Remove event handlers from the active editor + activeEditor.removeEventListener('keydown', handleKeydown); + } + } + } + function onInit(targetElm) { + $timeout(function () { + // Fetch the DOM nodes we need + getDomNodes(); + cleanupEventHandlers(); + getFocusableElements(targetElm); + if (focusableElements.length > 0) { + observeDomChanges(); + setElementFocus(); + // Handle keydown + target.addEventListener('keydown', handleKeydown); + } + }); + } + scope.$on('$includeContentLoaded', function () { + angularHelper.safeApply(scope, function () { + onInit(); + }); + }); + // If more than one editor is still open then re-initialize otherwise remove the event listener + scope.$on('$destroy', function () { + // Make sure to disconnect the observer so we potentially don't end up with having many active ones + disconnectObserver = true; + if (infiniteEditors && infiniteEditors.length > 1) { + // Pass the correct editor in order to find the focusable elements + var newTarget = infiniteEditors[infiniteEditors.length - 2]; + if (infiniteEditors.length > 1) { + // Setting closing till true will let us re-apply the last known focus to then opened layer that then becomes + // active + closingEditor = true; + onInit(newTarget); + return; + } + } + // Clear lastKnownFocusableElements + clearLastKnownFocusedElements(); + // Cleanup event handler + target.removeEventListener('keydown', handleKeydown); + }); + } + var directive = { + restrict: 'A', + link: link + }; + return directive; + } + angular.module('umbraco.directives').directive('umbFocusLock', FocusLock); + }()); + // TODO: Ensure the domObserver is NOT started when there is only one infinite overlay and it's being destroyed! + 'use strict'; /** @ngdoc directive @name umbraco.directives.directive:umbRadiobutton @@ -6845,20 +7706,26 @@ Use this directive to prevent default action of an element. Effectively implemen @param {string} value Set the value of the radiobutton. @param {string} name Set the name of the radiobutton. @param {string} text Set the text for the radiobutton label. -@param {string} labelKey Set a dictinary/localization string for the checkbox label +@param {string} labelKey Set a dictinary/localization string for the checkbox label. +@param {string} serverValidationField Set the val-server-field of the radiobutton. @param {boolean} disabled Set the radiobutton to be disabled. @param {boolean} required Set the radiobutton to be required. @param {callback} onChange Callback when the value of the radiobutton change by interaction. +@param {string} cssClass Set a css class modifier. +@param {string} iconClass Set an icon next to radiobutton. +@param {boolean} disableDirtyCheck Disable checking if the model is dirty. **/ (function () { 'use strict'; - function UmbRadiobuttonController($timeout) { + function UmbRadiobuttonController($timeout, $attrs, localizationService) { var vm = this; vm.$onInit = onInit; vm.change = change; function onInit() { vm.inputId = vm.inputId || 'umb-radio_' + String.CreateGuid(); + vm.disableDirtyCheck = $attrs.hasOwnProperty('disableDirtyCheck') && vm.disableDirtyCheck !== '0' && vm.disableDirtyCheck !== 0 && vm.disableDirtyCheck !== 'false' && vm.disableDirtyCheck !== false; + vm.icon = vm.icon || vm.iconClass || null; // If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [ if (vm.labelKey) { localizationService.localize(vm.labelKey).then(function (data) { @@ -6880,9 +7747,10 @@ Use this directive to prevent default action of an element. Effectively implemen } } var component = { - template: ' ', + template: ' ', controller: UmbRadiobuttonController, controllerAs: 'vm', + transclude: true, bindings: { model: '=', inputId: '@', @@ -6890,9 +7758,15 @@ Use this directive to prevent default action of an element. Effectively implemen name: '@', text: '@', labelKey: '@?', + serverValidationField: '@', disabled: '<', required: '<', - onChange: '&?' + onChange: '&?', + cssClass: '@?', + iconClass: '@?', + // deprecated + icon: '@?', + disableDirtyCheck: '=?' } }; angular.module('umbraco.directives').component('umbRadiobutton', component); @@ -6919,7 +7793,7 @@ will override element type to textarea and add own attribute ngModel tied to jso }, link: function link(scope, element, attrs, ngModelCtrl) { function setEditing(value) { - scope.jsonEditing = angular.copy(jsonToString(value)); + scope.jsonEditing = Utilities.copy(jsonToString(value)); } function updateModel(value) { scope.model = stringToJson(value); @@ -6932,7 +7806,7 @@ will override element type to textarea and add own attribute ngModel tied to jso } function stringToJson(text) { try { - return angular.fromJson(text); + return Utilities.fromJson(text); } catch (err) { setInvalid(); return text; @@ -6941,12 +7815,12 @@ will override element type to textarea and add own attribute ngModel tied to jso 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); + return Utilities.toJson(object, true); } function isValidJson(model) { var flag = true; try { - angular.fromJson(model); + Utilities.fromJson(model); } catch (err) { flag = false; } @@ -6984,6 +7858,117 @@ will override element type to textarea and add own attribute ngModel tied to jso }; }); 'use strict'; + /** +@ngdoc directive +@name umbraco.directives.directive:umbSearchFilter +@restrict E +@scope + +@description +Added in Umbraco version 8.7.0 Use this directive to render an umbraco search filter. + +

    Markup example

    +
    +    
    + + + + +
    +
    + +@param {boolean} model Set to true or false to set the checkbox to checked or unchecked. +@param {string} inputId Set the id of the checkbox. +@param {string} text Set the text for the checkbox label. +@param {string} labelKey Set a dictionary/localization string for the checkbox label +@param {callback} onChange Callback when the value of the searchbox change. +@param {callback} onBack Callback when clicking back button. +@param {boolean} autoFocus Add autofocus to the input field +@param {boolean} preventSubmitOnEnter Set the enter prevent directive or not +@param {boolean} showBackButton Show back button on search + +**/ + (function () { + 'use strict'; + function UmbSearchFilterController($timeout, localizationService) { + var vm = this; + vm.$onInit = onInit; + vm.change = change; + vm.keyDown = keyDown; + vm.blur = blur; + vm.goBack = goBack; + function onInit() { + vm.inputId = vm.inputId || 'umb-search-filter_' + String.CreateGuid(); + vm.autoFocus = Object.toBoolean(vm.autoFocus) === true; + vm.preventSubmitOnEnter = Object.toBoolean(vm.preventSubmitOnEnter) === true; + vm.showBackButton = Object.toBoolean(vm.showBackButton) === true; + // If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [ + if (vm.labelKey) { + localizationService.localize(vm.labelKey).then(function (data) { + if (data.indexOf('[') === -1) { + vm.text = data; + } + }); + } + } + function goBack() { + if (vm.onBack) { + vm.onBack(); + } + } + function change() { + if (vm.onChange) { + $timeout(function () { + vm.onChange({ + model: vm.model, + value: vm.value + }); + }, 0); + } + } + function blur() { + if (vm.onBlur) { + vm.onBlur(); + } + } + function keyDown(evt) { + //13: enter + switch (evt.keyCode) { + case 13: + if (vm.onSearch) { + vm.onSearch(); + } + break; + } + } + } + var component = { + template: '
    ', + controller: UmbSearchFilterController, + controllerAs: 'vm', + transclude: true, + bindings: { + model: '=', + inputId: '@', + text: '@', + labelKey: '@?', + onChange: '&?', + onSearch: '&?', + onBlur: '&?', + onBack: '&?', + autoFocus: 'true to hide the label. +@param {string=} alias The alias of the field within the control group. +@param {string=} labelFor The alias of the field that the label is for, used for validation. +@param {boolean=} required Set to true to mark the field as required. + **/ angular.module('umbraco.directives.html').directive('umbControlGroup', function (localizationService) { return { @@ -7320,7 +8327,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link transclude: true, restrict: 'E', replace: true, - template: '
    ', + template: '
    ', link: function link(scope, element, attr, formCtrl) { scope.formValid = function () { if (formCtrl && scope.labelFor) { @@ -7382,31 +8389,38 @@ Use this directive to construct a title. Recommended to use it inside an {@link * @restrict E * @function **/ - angular.module('umbraco.directives').directive('umbImageCrop', function ($timeout, cropperHelper) { + angular.module('umbraco.directives').directive('umbImageCrop', function ($timeout, cropperHelper, windowResizeListener) { + var MAX_SCALE = 4; return { restrict: 'E', replace: true, - template: '
    ', + transclude: true, + template: '
    {{width}}px x {{height}}px
    ', scope: { src: '=', width: '@', height: '@', crop: '=', center: '=', - maxSize: '@' + maxSize: '@?', + alias: '@?', + forceUpdate: '@?' }, link: function link(scope, element, attrs) { + var unsubscribe = []; var sliderRef = null; - scope.width = 400; - scope.height = 320; + scope.loaded = false; + scope.width = 0; + scope.height = 0; scope.dimensions = { + element: {}, image: {}, cropper: {}, viewport: {}, - margin: 20, + margin: {}, scale: { - min: 0, - max: 3, + min: 1, + max: MAX_SCALE, current: 1 } }; @@ -7416,10 +8430,10 @@ Use this directive to construct a title. Recommended to use it inside an {@link 'tooltips': [false], 'format': { to: function to(value) { - return parseFloat(parseFloat(value).toFixed(3)); //Math.round(value); + return parseFloat(parseFloat(value).toFixed(3)); }, from: function from(value) { - return parseFloat(parseFloat(value).toFixed(3)); //Math.round(value); + return parseFloat(parseFloat(value).toFixed(3)); } }, 'range': { @@ -7429,16 +8443,21 @@ Use this directive to construct a title. Recommended to use it inside an {@link }; scope.setup = function (slider) { sliderRef = slider; - // Set slider handle position - sliderRef.noUiSlider.set(scope.dimensions.scale.current); - // Update slider range min/max - sliderRef.noUiSlider.updateOptions({ - 'range': { - 'min': scope.dimensions.scale.min, - 'max': scope.dimensions.scale.max - } - }); + updateSlider(); }; + function updateSlider() { + if (sliderRef) { + // Update slider range min/max + sliderRef.noUiSlider.updateOptions({ + 'range': { + 'min': scope.dimensions.scale.min, + 'max': scope.dimensions.scale.max + } + }); + // Set slider handle position + sliderRef.noUiSlider.set(scope.dimensions.scale.current); + } + } scope.slide = function (values) { if (values) { scope.dimensions.scale.current = parseFloat(values); @@ -7449,98 +8468,111 @@ Use this directive to construct a title. Recommended to use it inside an {@link scope.dimensions.scale.current = parseFloat(values); } }; + function onScroll(event) { + // cross-browser wheel delta + var delta = Math.max(-50, Math.min(50, event.wheelDelta || -event.detail)); + if (sliderRef) { + var currentScale = sliderRef.noUiSlider.get(); + var newScale = Math.min(Math.max(currentScale + delta * 0.001 * scope.dimensions.image.ratio, scope.dimensions.scale.min), scope.dimensions.scale.max); + sliderRef.noUiSlider.set(newScale); + scope.$evalAsync(function () { + scope.dimensions.scale.current = newScale; + }); + if (event.preventDefault) { + event.preventDefault(); + } + } + } //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' + function updateStyles() { + scope.maskStyle = { + 'height': parseInt(scope.dimensions.cropper.height, 10) + 'px', + 'width': parseInt(scope.dimensions.cropper.width, 10) + 'px', + 'top': parseInt(scope.dimensions.margin.top, 10) + 'px', + 'left': parseInt(scope.dimensions.margin.left, 10) + 'px' }; - }; + } + ; + updateStyles(); //elements var $viewport = element.find('.viewport'); var $image = element.find('img'); var $overlay = element.find('.overlay'); - var $container = element.find('.crop-container'); + $overlay.bind('focus', function () { + $overlay.bind('DOMMouseScroll mousewheel onmousewheel', onScroll); + }); + $overlay.bind('blur', function () { + $overlay.unbind('DOMMouseScroll mousewheel onmousewheel', onScroll); + }); //default constraints for drag n drop var constraints = { left: { - max: scope.dimensions.margin, - min: scope.dimensions.margin + max: 0, + min: 0 }, top: { - max: scope.dimensions.margin, - min: scope.dimensions.margin + max: 0, + min: 0 } }; scope.constraints = constraints; //set constaints for cropping drag and drop var setConstraints = function setConstraints() { - 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; + constraints.left.min = scope.dimensions.cropper.width - scope.dimensions.image.width; + constraints.top.min = scope.dimensions.cropper.height - scope.dimensions.image.height; }; - var setDimensions = function setDimensions(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; + var setDimensions = function setDimensions() { + scope.dimensions.image.width = scope.dimensions.image.originalWidth; + scope.dimensions.image.height = scope.dimensions.image.originalHeight; //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; + var _cropW = parseInt(scope.width, 10); + var _cropH = parseInt(scope.height, 10); + var ratioCalculation = cropperHelper.scaleToMaxSize(_cropW, _cropH, scope.dimensions.viewport.width - 40, scope.dimensions.viewport.height - 40); + //so if we have a max size, override the thumb sizes + _cropW = ratioCalculation.width; + _cropH = ratioCalculation.height; + // set margins: + scope.dimensions.margin.left = (scope.dimensions.viewport.width - _cropW) * 0.5; + scope.dimensions.margin.top = (scope.dimensions.viewport.height - _cropH) * 0.5; + scope.dimensions.cropper.width = _cropW; + scope.dimensions.cropper.height = _cropH; + updateStyles(); }; //resize to a given ratio var resizeImageToScale = function resizeImageToScale(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; + var prevWidth = scope.dimensions.image.width; + var prevHeight = scope.dimensions.image.height; + scope.dimensions.image.width = scope.dimensions.image.originalWidth * ratio; + scope.dimensions.image.height = scope.dimensions.image.originalHeight * ratio; + var difW = scope.dimensions.image.width - prevWidth; + var difH = scope.dimensions.image.height - prevHeight; + // normalized focus point: + var focusNormX = (-scope.dimensions.image.left + scope.dimensions.cropper.width * 0.5) / prevWidth; + var focusNormY = (-scope.dimensions.image.top + scope.dimensions.cropper.height * 0.5) / prevHeight; + scope.dimensions.image.left = scope.dimensions.image.left - difW * focusNormX; + scope.dimensions.image.top = scope.dimensions.image.top - difH * focusNormY; setConstraints(); validatePosition(scope.dimensions.image.left, scope.dimensions.image.top); }; //resize the image to a predefined crop coordinate var resizeImageToCrop = function resizeImageToCrop() { - scope.dimensions.image = cropperHelper.convertToStyle(scope.crop, { + scope.dimensions.image = cropperHelper.convertToStyle(runtimeCrop, { width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight - }, scope.dimensions.cropper, scope.dimensions.margin); + }, scope.dimensions.cropper, 0); 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; // Update min and max based on original width/height + // Here we update the slider to use the scala of the current setup, i dont know why its made in this way but this is how it is. scope.dimensions.scale.min = ratioCalculation.ratio; - scope.dimensions.scale.max = 2; + // TODO: Investigate wether we can limit users to not scale bigger than the amount of pixels in the source: + //scope.dimensions.scale.max = ratioCalculation.ratio * Math.min(MAX_SCALE, scope.dimensions.image.originalWidth/scope.dimensions.cropper.width); + scope.dimensions.scale.max = ratioCalculation.ratio * MAX_SCALE; + updateSlider(); }; var validatePosition = function validatePosition(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; - } + left = Math.min(Math.max(left, constraints.left.min), constraints.left.max); + top = Math.min(Math.max(top, constraints.top.min), constraints.top.max); if (scope.dimensions.image.left !== left) { scope.dimensions.image.left = left; } @@ -7549,30 +8581,46 @@ Use this directive to construct a title. Recommended to use it inside an {@link } }; //sets scope.crop to the recalculated % based crop - var calculateCropBox = function calculateCropBox() { - scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); - }; + function calculateCropBox() { + runtimeCrop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, 0); + } + ; + function saveCropBox() { + scope.crop = Utilities.copy(runtimeCrop); + } //Drag and drop positioning, using jquery ui draggable - var onStartDragPosition, top, left; + //var onStartDragPosition, top, left; + var dragStartPosition = {}; $overlay.draggable({ + start: function start(event, ui) { + dragStartPosition.left = scope.dimensions.image.left; + dragStartPosition.top = scope.dimensions.image.top; + }, drag: function drag(event, ui) { scope.$apply(function () { - validatePosition(ui.position.left, ui.position.top); + validatePosition(dragStartPosition.left + (ui.position.left - ui.originalPosition.left), dragStartPosition.top + (ui.position.top - ui.originalPosition.top)); }); }, stop: function stop(event, ui) { scope.$apply(function () { //make sure that every validates one more time... - validatePosition(ui.position.left, ui.position.top); + validatePosition(dragStartPosition.left + (ui.position.left - ui.originalPosition.left), dragStartPosition.top + (ui.position.top - ui.originalPosition.top)); calculateCropBox(); - scope.dimensions.image.rnd = Math.random(); + saveCropBox(); }); } }); - var init = function init(image) { - scope.loaded = false; - //set dimensions on image, viewport, cropper etc - setDimensions(image); + var runtimeCrop; + var init = function init() { + // store original size: + scope.dimensions.image.originalWidth = $image.width(); + scope.dimensions.image.originalHeight = $image.height(); + // runtime Crop, should not be saved until we have interactions: + runtimeCrop = Utilities.copy(scope.crop); + onViewportSizeChanged(); + scope.loaded = true; + }; + function setCrop() { //create a default crop if we haven't got one already var createDefaultCrop = !scope.crop; if (createDefaultCrop) { @@ -7583,34 +8631,67 @@ Use this directive to construct a title. Recommended to use it inside an {@link if (createDefaultCrop) { scope.dimensions.scale.current = scope.dimensions.scale.min; resizeImageToScale(scope.dimensions.scale.min); + if (scope.center) { + // Move image to focal point if set + // Repeating a few calls here, but logic is too difficult to follow elsewhere + var x1 = Math.min(Math.max(scope.center.left * scope.dimensions.image.width - scope.dimensions.cropper.width / 2, 0), scope.dimensions.image.width - scope.dimensions.cropper.width); + var y1 = Math.min(Math.max(scope.center.top * scope.dimensions.image.height - scope.dimensions.cropper.height / 2, 0), scope.dimensions.image.height - scope.dimensions.cropper.height); + scope.dimensions.image.left = x1; + scope.dimensions.image.top = y1; + calculateCropBox(); + resizeImageToCrop(); + } } - //sets constaints for the cropper + } + function onViewportSizeChanged() { + scope.dimensions.viewport.width = $viewport.width(); + scope.dimensions.viewport.height = $viewport.height(); + setDimensions(); + setCrop(); setConstraints(); - scope.loaded = true; - }; + } // Watchers - scope.$watchCollection('[width, height]', function (newValues, oldValues) { + unsubscribe.push(scope.$watchCollection('[width, height, alias, forceUpdate]', function (newValues, oldValues) { // We have to reinit the whole thing if // one of the external params changes if (newValues !== oldValues) { - setDimensions($image); + runtimeCrop = Utilities.copy(scope.crop); + setDimensions(); + setCrop(); setConstraints(); } - }); - var throttledResizing = _.throttle(function () { - resizeImageToScale(scope.dimensions.scale.current); - calculateCropBox(); - }, 15); + })); + var throttledScale = _.throttle(function () { + return scope.$evalAsync(function () { + resizeImageToScale(scope.dimensions.scale.current); + calculateCropBox(); + saveCropBox(); + }); + }, 16); // Happens when we change the scale - scope.$watch('dimensions.scale.current', function (newValue, oldValue) { + unsubscribe.push(scope.$watch('dimensions.scale.current', function (newValue, oldValue) { if (scope.loaded) { - throttledResizing(); + throttledScale(); } - }); + })); // Init + //if we have a max-size we will use it, to keep this backwards compatible. + // I dont see this max size begin usefull, as we should aim for responsive UI. + if (scope.maxSize) { + element.css('max-width', parseInt(scope.maxSize, 10) + 'px'); + element.css('max-height', parseInt(scope.maxSize, 10) + 'px'); + } $image.on('load', function () { $timeout(function () { - init($image); + init(); + }); + }); + windowResizeListener.register(onViewportSizeChanged); + scope.$on('$destroy', function () { + $image.prop('src', ''); + windowResizeListener.unregister(onViewportSizeChanged); + unsubscribe.forEach(function (u) { + return u(); }); }); } @@ -7628,9 +8709,9 @@ Use this directive to construct a title. Recommended to use it inside an {@link left: 0, top: 0 }; - var htmlImage = null; + var imageElement = null; //DOM element reference - var htmlOverlay = null; + var focalPointElement = null; //DOM element reference var draggable = null; vm.loaded = false; @@ -7638,27 +8719,37 @@ Use this directive to construct a title. Recommended to use it inside an {@link vm.$onChanges = onChanges; vm.$postLink = postLink; vm.$onDestroy = onDestroy; - vm.style = style; + vm.style = {}; + vm.overlayStyle = {}; vm.setFocalPoint = setFocalPoint; + vm.resetFocalPoint = resetFocalPoint; /** Sets the css style for the Dot */ - function style() { - if (vm.dimensions.width <= 0 || vm.dimensions.height <= 0) { - //this initializes the dimensions since when the image element first loads - //there will be zero dimensions - setDimensions(); - } - return { + function updateStyle() { + vm.style = { 'top': vm.dimensions.top + 'px', 'left': vm.dimensions.left + 'px' }; + vm.overlayStyle = { + 'width': vm.dimensions.width + 'px', + 'height': vm.dimensions.height + 'px' + }; + } + ; + function resetFocalPoint() { + vm.onValueChanged({ + left: 0.5, + top: 0.5 + }); } ; function setFocalPoint(event) { $scope.$emit('imageFocalPointStart'); - var offsetX = event.offsetX - 10; - var offsetY = event.offsetY - 10; + // We do this to get the right position, no matter the focalPoint was clicked. + var viewportPosition = imageElement[0].getBoundingClientRect(); + var offsetX = event.clientX - viewportPosition.left; + var offsetY = event.clientY - viewportPosition.top; calculateGravity(offsetX, offsetY); - lazyEndEvent(); + $scope.$emit('imageFocalPointStop'); } ; /** Initializes the component */ @@ -7673,36 +8764,29 @@ Use this directive to construct a title. Recommended to use it inside an {@link /** Called when the component has linked everything and the DOM is available */ function postLink() { //elements - htmlImage = $element.find('img'); - htmlOverlay = $element.find('.overlay'); + imageElement = $element.find('img'); + focalPointElement = $element.find('.focalPoint'); //Drag and drop positioning, using jquery ui draggable - draggable = htmlOverlay.draggable({ + draggable = focalPointElement.draggable({ containment: 'parent', start: function start() { - $scope.$apply(function () { - $scope.$emit('imageFocalPointStart'); - }); + $scope.$emit('imageFocalPointStart'); }, - stop: function stop() { - $scope.$apply(function () { - var offsetX = htmlOverlay[0].offsetLeft; - var offsetY = htmlOverlay[0].offsetTop; - calculateGravity(offsetX, offsetY); - }); - lazyEndEvent(); + stop: function stop(event, ui) { + var offsetX = ui.position.left; + var offsetY = ui.position.top; + $scope.$evalAsync(calculateGravity(offsetX, offsetY)); + $scope.$emit('imageFocalPointStop'); } }); - $(window).on('resize.umbImageGravity', function () { - $scope.$apply(function () { - resized(); - }); - }); + window.addEventListener('resize.umbImageGravity', onResizeHandler); + window.addEventListener('resize', onResizeHandler); //if any ancestor directive emits this event, we need to resize $scope.$on('editors.content.splitViewChanged', function () { $timeout(resized, 200); }); //listen for the image DOM element loading - htmlImage.on('load', function () { + imageElement.on('load', function () { $timeout(function () { vm.isCroppable = true; vm.hasDimensions = true; @@ -7720,6 +8804,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link } } setDimensions(); + updateStyle(); vm.loaded = true; if (vm.onImageLoaded) { vm.onImageLoaded({ @@ -7731,40 +8816,54 @@ Use this directive to construct a title. Recommended to use it inside an {@link }); } function onDestroy() { - $(window).off('resize.umbImageGravity'); - if (htmlOverlay) { - } - if (htmlImage) { - htmlImage.off('load'); + window.removeEventListener('resize.umbImageGravity', onResizeHandler); + window.removeEventListener('resize', onResizeHandler); + /* + if (focalPointElement) { + // TODO: This should be destroyed but this will throw an exception: + // "cannot call methods on draggable prior to initialization; attempted to call method 'destroy'" + // I've tried lots of things and cannot get this to work, we weren't destroying before so hopefully + // there's no mem leaks? + focalPointElement.draggable("destroy"); + } + */ + if (imageElement) { + imageElement.off('load'); } } /** Called when we need to resize based on window or DOM dimensions to re-center the focal point */ function resized() { $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 (htmlOverlay.is(':visible')) { - var offsetX = htmlOverlay[0].offsetLeft; - var offsetY = htmlOverlay[0].offsetTop; - calculateGravity(offsetX, offsetY); - } + updateStyle(); + }); /* + // 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 (focalPointElement.is(':visible')) { + var offsetX = focalPointElement[0].offsetLeft; + var offsetY = focalPointElement[0].offsetTop; + calculateGravity(offsetX, offsetY); + } + */ + } + function onResizeHandler() { + $scope.$evalAsync(resized); } /** Watches the one way binding changes */ function onChanges(changes) { - if (changes.center && !changes.center.isFirstChange() && changes.center.currentValue && !angular.equals(changes.center.currentValue, changes.center.previousValue)) { + if (changes.center && !changes.center.isFirstChange() && changes.center.currentValue && !Utilities.equals(changes.center.currentValue, changes.center.previousValue)) { //when center changes update the dimensions setDimensions(); + updateStyle(); } } /** Sets the width/height/left/top dimentions based on the image size and the "center" value */ function setDimensions() { - if (vm.isCroppable && htmlImage && vm.center) { - vm.dimensions.width = htmlImage.width(); - vm.dimensions.height = htmlImage.height(); - vm.dimensions.left = vm.center.left * vm.dimensions.width - 10; - vm.dimensions.top = vm.center.top * vm.dimensions.height - 10; + if (vm.isCroppable && imageElement && vm.center) { + vm.dimensions.width = imageElement.width(); + vm.dimensions.height = imageElement.height(); + vm.dimensions.left = vm.center.left * vm.dimensions.width; + vm.dimensions.top = vm.center.top * vm.dimensions.height; } return vm.dimensions.width; } @@ -7776,25 +8875,20 @@ Use this directive to construct a title. Recommended to use it inside an {@link */ function calculateGravity(offsetX, offsetY) { vm.onValueChanged({ - left: (offsetX + 10) / vm.dimensions.width, - top: (offsetY + 10) / vm.dimensions.height - }); //vm.center.left = (offsetX + 10) / scope.dimensions.width; - //vm.center.top = (offsetY + 10) / scope.dimensions.height; + left: Math.min(Math.max(offsetX, 0), vm.dimensions.width) / vm.dimensions.width, + top: Math.min(Math.max(offsetY, 0), vm.dimensions.height) / vm.dimensions.height + }); } ; - var lazyEndEvent = _.debounce(function () { - $scope.$apply(function () { - $scope.$emit('imageFocalPointStop'); - }); - }, 2000); } var umbImageGravityComponent = { - template: '
    ', + template: '
    ', bindings: { src: '<', center: '<', onImageLoaded: '&?', - onValueChanged: '&' + onValueChanged: '&', + disableFocalPoint: '
    This Media item has no references.
    Used in Documents
    Name
    Alias
    Open
    {{::reference.name}}
    {{::reference.alias}}
    Used in Members
    Name
    Alias
    Open
    {{::reference.name}}
    {{::reference.alias}}
    Used in Media
    Name
    Alias
    Open
    {{::reference.name}}
    {{::reference.alias}}
    {{node.createDateFormatted}} by {{ node.owner.name }} {{node.updateDateFormatted}}
    {{ node.id }}
    {{ node.key }}
    ', + template: '
    This Media item has no references.
    Used in Documents
    Name
    Alias
    Open
    {{::reference.name}}
    {{::reference.alias}}
    Used in Members
    Name
    Alias
    Open
    {{::reference.name}}
    {{::reference.alias}}
    Used in Media
    Name
    Alias
    Open
    {{::reference.name}}
    {{::reference.alias}}
    {{node.createDateFormatted}} by {{ node.owner.name }} {{node.updateDateFormatted}}
    {{ node.id }}
    {{ node.key }}
    ', scope: { node: '=' }, link: link }; @@ -8164,6 +9258,16 @@ Use this directive to construct a title. Recommended to use it inside an {@link //TODO: Infinite editing is not working yet. scope.allowChangeMemberType = false; function onInit() { + userService.getCurrentUser().then(function (user) { + // only allow change of member type if user has access to the settings sections + Utilities.forEach(user.sections, function (section) { + if (section.alias === 'settings') { + scope.allowChangeMemberType = true; + } + }); + }); + // get member type details + scope.memberType = scope.node.contentType; // make sure dates are formatted to the user's locale formatDatesToLocal(); } @@ -8208,7 +9312,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link var directive = { restrict: 'E', replace: true, - template: '
    {{ group.label }}
    {{node.createDateFormatted}} by {{ node.owner.name }} {{node.updateDateFormatted}}
    {{ node.id }}
    {{ node.key }}
    ', + template: '
    {{ group.label }}
    {{node.createDateFormatted}} by {{ node.owner.name }} {{node.updateDateFormatted}}
    {{ node.id }}
    {{ node.key }}
    ', scope: { node: '=' }, link: link }; @@ -8236,7 +9340,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link var directive = { restrict: 'E', replace: true, - template: '
    ', + template: '
    ', link: link }; return directive; @@ -8453,6 +9557,9 @@ Opens an overlay to show a custom YSOD.
    setButtonText(); modelCopy = makeModelCopy(scope.model); $timeout(function () { + if (!scope.name) { + scope.name = 'overlay'; + } if (scope.position === 'target' && scope.model.event) { setTargetPosition(); // update the position of the overlay on content changes @@ -8500,7 +9607,7 @@ Opens an overlay to show a custom YSOD.
    templateScope.model = scope.model; element.html(response.data); element.show(); - $compile(element.contents())(templateScope); + $compile(element)(templateScope); }); } } @@ -8567,7 +9674,7 @@ Opens an overlay to show a custom YSOD.
    var newObject = {}; for (var key in object) { if (key !== 'event' && key !== 'parentScope') { - newObject[key] = angular.copy(object[key]); + newObject[key] = Utilities.copy(object[key]); } } return newObject; @@ -8583,11 +9690,13 @@ Opens an overlay to show a custom YSOD.
    } } 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 overlay = $(scope.model.event.target).closest('.umb-overlay'); + var container = overlay.length > 0 ? overlay : $('#contentwrapper'); + var rect = container[0].getBoundingClientRect(); + var containerLeft = rect.left; + var containerRight = containerLeft + rect.width; + var containerTop = rect.top; + var containerBottom = containerTop + rect.height; var mousePositionClickX = null; var mousePositionClickY = null; var elementHeight = null; @@ -8605,8 +9714,9 @@ Opens an overlay to show a custom YSOD.
    elementHeight = el[0].clientHeight; elementWidth = el[0].clientWidth; // move element to this position - position.left = mousePositionClickX - elementWidth / 2; - position.top = mousePositionClickY - elementHeight / 2; + // when using hotkey it fallback to center of container + position.left = mousePositionClickX ? mousePositionClickX - elementWidth / 2 : (containerLeft + containerRight) / 2 - elementWidth / 2; + position.top = mousePositionClickY ? mousePositionClickY - elementHeight / 2 : (containerTop + containerBottom) / 2 - elementHeight / 2; // check to see if element is outside screen // outside right if (position.left + elementWidth > containerRight) { @@ -8629,12 +9739,14 @@ Opens an overlay to show a custom YSOD.
    position.bottom = 'inherit'; } el.css(position); + el.css('visibility', 'visible'); } scope.submitForm = function (model) { if (scope.model.submit) { if (formHelper.submitForm({ scope: scope, - skipValidation: scope.model.skipFormValidation + skipValidation: scope.model.skipFormValidation, + keepServerValidation: true })) { if (scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { //wrap in a when since we don't know if this is a promise or not @@ -8681,13 +9793,14 @@ Opens an overlay to show a custom YSOD.
    transclude: true, restrict: 'E', replace: true, - template: ' ', + template: ' ', scope: { ngShow: '=', model: '=', view: '=', position: '@', size: '=?', + name: '=?', parentScope: '=?' }, link: link @@ -8725,42 +9838,137 @@ Opens an overlay to show a custom YSOD.
    * @name umbraco.directives.directive:umbProperty * @restrict E **/ - angular.module('umbraco.directives').directive('umbProperty', function (userService) { - return { - scope: { + (function () { + 'use strict'; + angular.module('umbraco.directives').component('umbProperty', { + template: '
    {{vm.inheritsFrom}}
    ', + controller: UmbPropertyController, + controllerAs: 'vm', + transclude: true, + require: { + parentUmbProperty: '?^^umbProperty', + parentForm: '?^^form' + }, + bindings: { property: '=', + elementKey: '@', + // optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/) + propertyAlias: '@', showInherit: '<', inheritsFrom: '<' - }, - transclude: true, - restrict: 'E', - replace: true, - template: '
    {{inheritsFrom}}
    ', - link: function link(scope) { - scope.controlLabelTitle = null; + } + }); + function UmbPropertyController($scope, userService, serverValidationManager, udiService, angularHelper) { + var vm = this; + vm.$onInit = onInit; + vm.setDirty = function () { + // NOTE: We need to use scope because we haven't changd it to vm.propertyForm in the html and that + // might mean a breaking change. + $scope.propertyForm.$setDirty(); + }; + vm.setPropertyError = function (errorMsg) { + vm.property.propertyErrorMessage = errorMsg; + }; + vm.propertyActions = []; + vm.setPropertyActions = function (actions) { + vm.propertyActions = actions; + }; + // returns the validation path for the property to be used as the validation key for server side validation logic + vm.getValidationPath = function () { + var parentValidationPath = vm.parentUmbProperty ? vm.parentUmbProperty.getValidationPath() : null; + var propAlias = vm.propertyAlias ? vm.propertyAlias : vm.property.alias; + // the elementKey will be empty when this is not a nested property + var valPath = vm.elementKey ? vm.elementKey + '/' + propAlias : propAlias; + return serverValidationManager.createPropertyValidationKey(valPath, parentValidationPath); + }; + function onInit() { + vm.controlLabelTitle = null; if (Umbraco.Sys.ServerVariables.isDebuggingEnabled) { userService.getCurrentUser().then(function (u) { if (u.allowedSections.indexOf('settings') !== -1 ? true : false) { - scope.controlLabelTitle = scope.property.alias; + vm.controlLabelTitle = vm.property.alias; } }); } - }, - //Define a controller for this directive to expose APIs to other directives - controller: function controller($scope) { - var self = this; - //set the API properties/methods - self.property = $scope.property; - self.setPropertyError = function (errorMsg) { - $scope.property.propertyErrorMessage = errorMsg; - }; - $scope.propertyActions = []; - self.setPropertyActions = function (actions) { - $scope.propertyActions = actions; - }; + if (!vm.parentUmbProperty) { + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain($scope, function (s) { + return s && s.vm && s.vm.constructor.name === 'UmbPropertyController'; + }); + vm.parentUmbProperty = found ? found.vm : null; + } + } + } + }()); + 'use strict'; + (function () { + 'use strict'; + /** + * A component to render the property action toggle + */ + function umbPropertyActionsController(keyboardService, localizationService) { + var vm = this; + vm.isOpen = false; + vm.labels = { + openText: 'Open Property Actions', + closeText: 'Close Property Actions' + }; + vm.open = open; + vm.close = close; + vm.toggle = toggle; + vm.executeAction = executeAction; + vm.$onDestroy = onDestroy; + vm.$onInit = onInit; + function initDropDown() { + keyboardService.bind('esc', vm.close); + } + function destroyDropDown() { + keyboardService.unbind('esc'); + } + function toggle() { + if (vm.isOpen === true) { + vm.close(); + } else { + vm.open(); + } + } + function open() { + vm.isOpen = true; + initDropDown(); + } + function close() { + vm.isOpen = false; + destroyDropDown(); + } + function executeAction(action) { + action.method(); + vm.close(); + } + function onDestroy() { + if (vm.isOpen === true) { + destroyDropDown(); + } + } + function onInit() { + var labelKeys = [ + 'propertyActions_tooltipForPropertyActionsMenu', + 'propertyActions_tooltipForPropertyActionsMenuClose' + ]; + localizationService.localizeMany(labelKeys).then(function (values) { + vm.labels.openText = values[0]; + vm.labels.closeText = values[1]; + }); } + } + var umbPropertyActionsComponent = { + template: '
    ', + bindings: { actions: '<' }, + controllerAs: 'vm', + controller: umbPropertyActionsController }; - }); + angular.module('umbraco.directives').component('umbPropertyActions', umbPropertyActionsComponent); + }()); 'use strict'; /** * @ngdoc directive @@ -8794,7 +10002,12 @@ Opens an overlay to show a custom YSOD.
    if (!scope.model.alias) { scope.model.alias = Math.random().toString(36).slice(2); } - scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue); + var unbindWatcher = scope.$watch('model.view', function () { + scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue); + }); + scope.$on('$destroy', function () { + unbindWatcher(); + }); } }; } @@ -8845,24 +10058,27 @@ Use this directive to render a tabs navigation.
         
    - - + + - -
    -
    Content of tab 1
    -
    -
    -
    Content of tab 2
    -
    -
    + + + +
    +
    Content of tab 1
    +
    +
    +
    Content of tab 2
    +
    +
    +
    @@ -8871,7 +10087,7 @@ Use this directive to render a tabs navigation. (function () { "use strict"; - function Controller() { + function Controller(eventsService) { var vm = this; @@ -8896,7 +10112,7 @@ Use this directive to render a tabs navigation. selectedTab.active = true; }; - eventsService.on("tab.tabChange", function(name, args){ + eventsService.on("app.tabChange", function(name, args){ console.log("args", args); }); @@ -8919,16 +10135,19 @@ Use this directive to render a tabs navigation. **/ (function () { 'use strict'; - function TabsNavDirective($timeout, $window) { + function TabsNavDirective($timeout, $window, eventsService) { function link(scope, element, attrs, ctrl) { var tabNavItemsWidths = []; // the parent is the component itself so we need to go one level higher var container = element.parent().parent(); + var ro = new ResizeObserver(function () { + calculateWidth(); + }); + ro.observe(container[0]); $timeout(function () { element.find('li:not(umb-tab--expand)').each(function () { tabNavItemsWidths.push($(this).outerWidth()); }); - calculateWidth(); }); function calculateWidth() { $timeout(function () { @@ -8951,11 +10170,8 @@ Use this directive to render a tabs navigation. } }); } - $(window).on('resize.tabsNav', function () { - calculateWidth(); - }); scope.$on('$destroy', function () { - $(window).off('resize.tabsNav'); + ro.unobserve(container[0]); }); } function UmbTabsNavController(eventsService) { @@ -8990,7 +10206,7 @@ Use this directive to render a tabs navigation. var directive = { restrict: 'E', transclude: true, - template: ' ', + template: '
    ', link: link, bindToController: true, controller: UmbTabsNavController, @@ -9013,7 +10229,7 @@ Use this directive to render a tabs navigation. 'use strict'; angular.module('umbraco.directives').component('umbTagsEditor', { transclude: true, - template: '
    Loading...
    ', + template: '
    Loading...
    ', controller: umbTagsEditorController, controllerAs: 'vm', bindings: { @@ -9143,7 +10359,7 @@ Use this directive to render a tabs navigation. } function configureViewModel(isInitLoad) { if (vm.value) { - if (angular.isString(vm.value) && vm.value.length > 0) { + if (Utilities.isString(vm.value) && vm.value.length > 0) { if (vm.config.storageType === 'Json') { //json storage vm.viewModel = JSON.parse(vm.value); @@ -9169,7 +10385,7 @@ Use this directive to render a tabs navigation. updateModelValue(vm.viewModel); } } - } else if (angular.isArray(vm.value)) { + } else if (Utilities.isArray(vm.value)) { vm.viewModel = vm.value; } } @@ -9259,7 +10475,7 @@ Use this directive to render a tabs navigation. 'use strict'; (function () { 'use strict'; - function UmbContextDialog(navigationService, keyboardService, localizationService, overlayService) { + function UmbContextDialog(navigationService, keyboardService, localizationService, overlayService, backdropService) { function link($scope) { $scope.dialog = { confirmDiscardChanges: false }; $scope.outSideClick = function () { @@ -9306,7 +10522,7 @@ Use this directive to render a tabs navigation. var directive = { restrict: 'E', transclude: true, - template: '

    {{dialogTitle}}

    ', + template: ' ', scope: { dialogTitle: '<', currentNode: '<', @@ -9324,12 +10540,12 @@ Use this directive to render a tabs navigation. * @name umbraco.directives.directive:umbTree * @restrict E **/ - function umbTreeDirective($q, $rootScope, treeService, notificationsService, userService) { + function umbTreeDirective($q, treeService, notificationsService) { return { restrict: 'E', replace: true, terminal: false, - template: ' ', + template: ' ', scope: { section: '@', treealias: '@', @@ -9401,9 +10617,10 @@ Use this directive to render a tabs navigation. var lastSection = ''; /** Helper function to emit tree events */ function emitEvent(eventName, args) { - if (registeredCallbacks[eventName] && angular.isArray(registeredCallbacks[eventName])) { - _.each(registeredCallbacks[eventName], function (c) { - c(args); //call it + if (registeredCallbacks[eventName] && Utilities.isArray(registeredCallbacks[eventName])) { + // call it + registeredCallbacks[eventName].forEach(function (c) { + return c(args); }); } } @@ -9412,7 +10629,7 @@ Use this directive to render a tabs navigation. * @param {any} args either a string representing the 'section' or an object containing: 'section', 'treeAlias', 'customTreeParams', 'cacheKey' */ function load(args) { - if (angular.isString(args)) { + if (Utilities.isString(args)) { $scope.section = args; } else if (args) { if (args.section) { @@ -9451,7 +10668,7 @@ Use this directive to render a tabs navigation. if (!args.path) { throw 'args.path cannot be null'; } - if (angular.isString(args.path)) { + if (Utilities.isString(args.path)) { args.path = args.path.replace('"', '').split(','); } //Filter the path for root node ids (we don't want to pass in -1 or 'init') @@ -9626,14 +10843,14 @@ Use this directive to render a tabs navigation. // 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); + node.cssClasses.forEach(function (c) { + return css.push(c); }); } return css.join(' '); }; $scope.selectEnabledNodeClass = function (node) { - return node ? node.selected ? 'icon umb-tree-icon sprTree icon-check green temporary' : '' : ''; + return node && node.selected ? 'icon sprTree icon-check green temporary' : '-hidden'; }; /* helper to force reloading children of a tree node */ $scope.loadChildren = function (node, forceReload) { @@ -9698,8 +10915,8 @@ Use this directive to render a tabs navigation. //load the tree loadTree().then(function () { //because angular doesn't return a promise for the resolve method, we need to resort to some hackery, else - //like normal JS promises we could do resolve(...).then() - if (args && args.onLoaded && angular.isFunction(args.onLoaded)) { + //like normal JS promises we could do resolve(...).then() + if (args && args.onLoaded && Utilities.isFunction(args.onLoaded)) { args.onLoaded(); } }); @@ -9728,12 +10945,12 @@ Use this directive to render a tabs navigation. */ - angular.module('umbraco.directives').directive('umbTreeItem', function (treeService, $timeout, localizationService, eventsService, appState) { + angular.module('umbraco.directives').directive('umbTreeItem', function (treeService, $timeout, localizationService, eventsService, appState, navigationService) { return { restrict: 'E', replace: true, require: '^umbTree', - template: '
  • {{node.name}}
  • ', + template: '
  • {{node.name}}
  • ', scope: { section: '@', currentNode: '=', @@ -9773,8 +10990,8 @@ Use this directive to render a tabs navigation. var actionNode = appState.getMenuState('currentNode'); var css = []; if (node.cssClasses) { - _.each(node.cssClasses, function (c) { - css.push(c); + node.cssClasses.forEach(function (c) { + return css.push(c); }); } if (node.selected) { @@ -9896,13 +11113,19 @@ Use this directive to render a tabs navigation. scope.loadChildren(scope.node); } var evts = []; - //listen for section changes + // Listen for section changes evts.push(eventsService.on('appState.sectionState.changed', function (e, args) { if (args.key === 'currentSection') { //when the section changes disable all delete animations scope.node.deleteAnimations = false; } })); + // Update tree icon if changed + evts.push(eventsService.on('editors.tree.icon.changed', function (e, args) { + if (args.icon !== scope.node.icon && args.id === scope.node.id) { + scope.node.icon = args.icon; + } + })); /** Depending on if any menu is shown and if the menu is shown for the current node, toggle delete animations */ function toggleDeleteAnimations() { //if both are false then remove animations @@ -9942,7 +11165,7 @@ Use this directive to render a tabs navigation. * @element ANY * @restrict E **/ - function treeSearchBox(localizationService, searchService, $q) { + function treeSearchBox($q, searchService) { return { scope: { searchFromId: '@', @@ -9951,22 +11174,21 @@ Use this directive to render a tabs navigation. section: '@', datatypeKey: '@', hideSearchCallback: '=', - searchCallback: '=' + searchCallback: '=', + inputId: '@', + autoFocus: '=' }, restrict: 'E', // restrict to an element replace: true, // replace the html element with the template - template: '', + template: ' ', link: function link(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'; } @@ -10033,14 +11255,16 @@ Use this directive to render a tabs navigation. return { scope: { results: '=', - selectResultCallback: '=' + selectResultCallback: '=', + emptySearchResultPosition: '@' }, restrict: 'E', // restrict to an element replace: true, // replace the html element with the template - template: ' ', + template: '

    Sorry, we can not find what you are looking for.

    1 item returned

    {{results.length}} items returned

    ', link: function link(scope, element, attrs, ctrl) { + scope.emptySearchResultPosition = scope.emptySearchResultPosition || 'center'; } }; } @@ -10069,37 +11293,37 @@ Use this directive to render a tabs navigation. var setOptions = function setOptions(acee, session, opts) { // sets the ace worker path, if running from concatenated // or minified source - if (angular.isDefined(opts.workerPath)) { + if (Utilities.isDefined(opts.workerPath)) { var config = window.ace.require('ace/config'); config.set('workerPath', opts.workerPath); } // ace requires loading - if (angular.isDefined(opts.require)) { + if (Utilities.isDefined(opts.require)) { opts.require.forEach(function (n) { window.ace.require(n); }); } // Boolean options - if (angular.isDefined(opts.showGutter)) { + if (Utilities.isDefined(opts.showGutter)) { acee.renderer.setShowGutter(opts.showGutter); } - if (angular.isDefined(opts.useWrapMode)) { + if (Utilities.isDefined(opts.useWrapMode)) { session.setUseWrapMode(opts.useWrapMode); } - if (angular.isDefined(opts.showInvisibles)) { + if (Utilities.isDefined(opts.showInvisibles)) { acee.renderer.setShowInvisibles(opts.showInvisibles); } - if (angular.isDefined(opts.showIndentGuides)) { + if (Utilities.isDefined(opts.showIndentGuides)) { acee.renderer.setDisplayIndentGuides(opts.showIndentGuides); } - if (angular.isDefined(opts.useSoftTabs)) { + if (Utilities.isDefined(opts.useSoftTabs)) { session.setUseSoftTabs(opts.useSoftTabs); } - if (angular.isDefined(opts.showPrintMargin)) { + if (Utilities.isDefined(opts.showPrintMargin)) { acee.setShowPrintMargin(opts.showPrintMargin); } // commands - if (angular.isDefined(opts.disableSearch) && opts.disableSearch) { + if (Utilities.isDefined(opts.disableSearch) && opts.disableSearch) { acee.commands.addCommands([{ name: 'unfind', bindKey: { @@ -10113,23 +11337,23 @@ Use this directive to render a tabs navigation. }]); } // Basic options - if (angular.isString(opts.theme)) { + if (Utilities.isString(opts.theme)) { acee.setTheme('ace/theme/' + opts.theme); } - if (angular.isString(opts.mode)) { + if (Utilities.isString(opts.mode)) { session.setMode('ace/mode/' + opts.mode); } // Advanced options - if (angular.isDefined(opts.firstLineNumber)) { - if (angular.isNumber(opts.firstLineNumber)) { + if (Utilities.isDefined(opts.firstLineNumber)) { + if (Utilities.isNumber(opts.firstLineNumber)) { session.setOption('firstLineNumber', opts.firstLineNumber); - } else if (angular.isFunction(opts.firstLineNumber)) { + } else if (Utilities.isFunction(opts.firstLineNumber)) { session.setOption('firstLineNumber', opts.firstLineNumber()); } } // advanced options var key, obj; - if (angular.isDefined(opts.advanced)) { + if (Utilities.isDefined(opts.advanced)) { for (key in opts.advanced) { // create a javascript object with the key and value obj = { @@ -10141,7 +11365,7 @@ Use this directive to render a tabs navigation. } } // advanced options for the renderer - if (angular.isDefined(opts.rendererOptions)) { + if (Utilities.isDefined(opts.rendererOptions)) { for (key in opts.rendererOptions) { // create a javascript object with the key and value obj = { @@ -10153,8 +11377,8 @@ Use this directive to render a tabs navigation. } } // onLoad callbacks - angular.forEach(opts.callbacks, function (cb) { - if (angular.isFunction(cb)) { + Utilities.forEach(opts.callbacks, function (cb) { + if (Utilities.isFunction(cb)) { cb(acee); } }); @@ -10165,7 +11389,7 @@ Use this directive to render a tabs navigation. 'lib/ace-builds/src-min-noconflict/ace.js', 'lib/ace-builds/src-min-noconflict/ext-language_tools.js' ], scope).then(function () { - if (angular.isUndefined(window.ace)) { + if (Utilities.isUndefined(window.ace)) { throw new Error('ui-ace need ace to work... (o rly?)'); } else { // init editor @@ -10182,7 +11406,7 @@ Use this directive to render a tabs navigation. * umbAceEditorConfig merged with user options via json in attribute or data binding * @type object */ - var opts = angular.extend({}, options, scope.umbAceEditor); + var opts = Utilities.extend({}, options, scope.umbAceEditor); //load ace libraries here... /** * ACE editor @@ -10232,9 +11456,9 @@ Use this directive to render a tabs navigation. * @type {Array} */ var args = Array.prototype.slice.call(arguments, 1); - if (angular.isDefined(callback)) { + if (Utilities.isDefined(callback)) { scope.$evalAsync(function () { - if (angular.isFunction(callback)) { + if (Utilities.isFunction(callback)) { callback(args); } else { throw new Error('ui-ace use a function as callback.'); @@ -10292,7 +11516,7 @@ Use this directive to render a tabs navigation. if (current === previous) { return; } - opts = angular.extend({}, options, scope.umbAceEditor); + opts = Utilities.extend({}, options, scope.umbAceEditor); opts.callbacks = [opts.onLoad]; if (opts.onLoad !== options.onLoad) { // also call the global onLoad handler @@ -10398,15 +11622,17 @@ Use this directive to render an avatar. **/ (function () { 'use strict'; - function AvatarDirective() { + function AvatarDirective(localizationService) { function link(scope, element, attrs, ctrl) { var eventBindings = []; scope.initials = ''; + scope.avatarAlt = ''; function onInit() { if (!scope.unknownChar) { scope.unknownChar = '?'; } scope.initials = getNameInitials(scope.name); + setAvatarAlt(scope.name); } function getNameInitials(name) { if (name) { @@ -10418,6 +11644,14 @@ Use this directive to render an avatar. } return null; } + function setAvatarAlt(name) { + if (name) { + localizationService.localize('general_avatar').then(function (data) { + scope.avatarAlt = data + ' ' + name; + }); + } + scope.avatarAlt = null; + } eventBindings.push(scope.$watch('name', function (newValue, oldValue) { if (newValue === oldValue) { return; @@ -10426,13 +11660,14 @@ Use this directive to render an avatar. return; } scope.initials = getNameInitials(newValue); + setAvatarAlt(newValue); })); onInit(); } var directive = { restrict: 'E', replace: true, - template: '
    {{ initials }} {{unknownChar}}
    ', + template: '
    {{avatarAlt}}
    {{ initials }} {{unknownChar}}
    ', scope: { size: '@', name: '@', @@ -10497,7 +11732,7 @@ Use this directive to render a badge. restrict: 'E', replace: true, transclude: true, - template: ' ', + template: '
    ', scope: { size: '@?', checked: '=', @@ -10532,14 +11767,6 @@ Use this directive to render a ui component for selecting child items to a paren on-remove="vm.removeChild"> - - - - @@ -10548,7 +11775,7 @@ Use this directive to render a ui component for selecting child items to a paren (function () { "use strict"; - function Controller() { + function Controller(overlayService) { var vm = this; @@ -10575,23 +11802,29 @@ Use this directive to render a ui component for selecting child items to a paren vm.removeChild = removeChild; function addChild($event) { - vm.overlay = { + + const dialog = { 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); + + if (model.selectedItem) { + // add selected child + vm.selectedChildren.push(model.selectedItem); + } // close overlay - vm.overlay.show = false; - vm.overlay = null; + overlayService.close(); + }, + close: function() { + overlayService.close(); } }; + + overlayService.open(dialog); } function removeChild($index) { @@ -10610,13 +11843,18 @@ Use this directive to render a ui component for selecting child items to a paren @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. +@param {callback} onRemove (binding): Callback when removing 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. +@param {callback} onAdd (binding): Callback when adding an item. +

    The callback returns:

    +
      +
    • $event: The select event.
    • +
    +@param {callback} onSort (binding): Callback when sorting an item.

    The callback returns:

    • $event: The select event.
    • @@ -10641,13 +11879,13 @@ Use this directive to render a ui component for selecting child items to a paren }; function syncParentName() { // update name on available item - angular.forEach(scope.availableChildren, function (availableChild) { + Utilities.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) { + Utilities.forEach(scope.selectedChildren, function (selectedChild) { if (selectedChild.id === scope.parentId) { selectedChild.name = scope.parentName; } @@ -10655,13 +11893,13 @@ Use this directive to render a ui component for selecting child items to a paren } function syncParentIcon() { // update icon on available item - angular.forEach(scope.availableChildren, function (availableChild) { + Utilities.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) { + Utilities.forEach(scope.selectedChildren, function (selectedChild) { if (selectedChild.id === scope.parentId) { selectedChild.icon = scope.parentIcon; } @@ -10688,6 +11926,7 @@ Use this directive to render a ui component for selecting child items to a paren // sortable options for allowed child content types scope.sortableOptions = { axis: 'y', + cancel: '.unsortable', containment: 'parent', distance: 10, opacity: 0.7, @@ -10711,7 +11950,7 @@ Use this directive to render a ui component for selecting child items to a paren var directive = { restrict: 'E', replace: true, - template: '
      {{ parentName }} ()
      {{ selectedChild.name }}
      Add Child
      ', + template: '
      {{parentName}} ()
      {{selectedChild.name}}
      ', scope: { selectedChildren: '=', availableChildren: '=', @@ -10971,50 +12210,274 @@ Use this directive to render a ui component for selecting child items to a paren 'use strict'; /** @ngdoc directive -@name umbraco.directives.directive:umbColorSwatches +@name umbraco.directives.directive:umbColorPicker @restrict E @scope + @description -Use this directive to generate color swatches to pick from. +Added in Umbraco v. 8.10: Use this directive to render a color picker. +

      Markup example

      -    
      -    
      +    
      + + + + +
      -@param {array} colors (attribute): The array of colors. -@param {string} selectedColor (attribute): The selected color. -@param {string} size (attribute): The size (s, m). -@param {string} useLabel (attribute): Specify if labels should be used. -@param {string} useColorClass (attribute): Specify if color values are css classes. -@param {string} colorClassNamePrefix (attribute): Specify the prefix used for the class for each color (defaults to "btn"). -@param {function} onSelect (expression): Callback function when the item is selected. + +

      Controller example

      +
      +    (function () {
      +        "use strict";
      +    
      +        function Controller() {
      +    
      +            var vm = this;
      +
      +            vm.options = {
      +                type: "color",
      +                color: defaultColor,
      +                showAlpha: false,
      +                showPalette: true,
      +                showPaletteOnly: false,
      +                preferredFormat: "hex",
      +            };
      +            
      +            vm.show = show;
      +            vm.hide = hide;
      +            vm.change = change;
      +
      +            function show(color) {
      +                color.toHexString().trimStart("#");
      +            }
      +
      +            function hide(color) {
      +                color.toHexString().trimStart("#");
      +            }
      +
      +            function change(color) {
      +                color.toHexString().trimStart("#");
      +            }
      +        }
      +    
      +        angular.module("umbraco").controller("My.ColorController", Controller);
      +    
      +    })();
      +
      + +@param {string} ngModel (binding): Value for the color picker. +@param {object} options (binding): Config object for the color picker. +@param {function} onBeforeShow (expression): You can prevent the color picker from showing up if you return false in the beforeShow event. This event is ignored on a flat color picker. +@param {function} onChange (expression): Called as the original input changes. Only happens when the input is closed or the 'Choose' button is clicked. +@param {function} onShow (expression): Called after the color picker is opened. This is ignored on a flat color picker. Note, when any color picker on the page is shown it will hide any that are already open. +@param {function} onHide (expression): Called after the color picker is hidden. This happens when clicking outside of the picker while it is open. Note, when any color picker on the page is shown it will hide any that are already open. This event is ignored on a flat color picker. +@param {function} onMove (expression): Called as the user moves around within the color picker. +@param {function} onDragStart (expression): Called at the beginning of a drag event on either hue slider, alpha slider, or main color picker areas. +@param {function} onDragStop (expression): Called at the end of a drag event on either hue slider, alpha slider, or main color picker areas. + **/ (function () { 'use strict'; - function ColorSwatchesDirective() { - function link(scope, el, attr, ctrl) { - // Set default to true if not defined - if (angular.isUndefined(scope.useColorClass)) { - scope.useColorClass = false; - } - // Set default to "btn" if not defined - if (angular.isUndefined(scope.colorClassNamePrefix)) { - scope.colorClassNamePrefix = 'btn'; + function ColorPickerController($scope, $element, $timeout, assetsService, localizationService) { + var ctrl = this; + var colorPickerInstance = null; + var labels = {}; + ctrl.$onInit = function () { + // load the separate css for the editor to avoid it blocking our js loading + assetsService.loadCss('lib/spectrum/spectrum.css', $scope); + // load the js file for the color picker + assetsService.load([//"lib/spectrum/tinycolor.js", + 'lib/spectrum/spectrum.js'], $scope).then(function () { + // init color picker + grabElementAndRun(); + }); + }; + ctrl.$onChanges = function (changes) { + if (colorPickerInstance && changes.ngModel) { + colorPickerInstance.spectrum('set', changes.ngModel.currentValue); } - scope.setColor = function (color, $index, $event) { - if (scope.onSelect) { - // did the value change? - if (scope.selectedColor != null && scope.selectedColor.value === color.value) { - // User clicked the currently selected color - // to remove the selection, they don't want - // to select any color after all. - // Unselect the color - color = null; - } - scope.selectedColor = color; + }; + function grabElementAndRun() { + var labelKeys = [ + 'general_cancel', + 'general_choose', + 'general_clear' + ]; + localizationService.localizeMany(labelKeys).then(function (values) { + labels.cancel = values[0]; + labels.choose = values[1]; + labels.clear = values[2]; + }); + $timeout(function () { + var element = $element.find('.umb-color-picker > input')[0]; + setColorPicker(element, labels); + }, 0, true); + } + function setColorPicker(element, labels) { + // Spectrum options: https://seballot.github.io/spectrum/#options + var defaultOptions = { + type: 'color', + color: null, + showAlpha: false, + showInitial: false, + showInput: true, + cancelText: labels.cancel, + clearText: labels.clear, + chooseText: labels.choose, + preferredFormat: 'hex', + clickoutFiresChange: true + }; + // If has ngModel set the color + if (ctrl.ngModel) { + defaultOptions.color = ctrl.ngModel; + } + //const options = ctrl.options ? ctrl.options : defaultOptions; + var options = Utilities.extend(defaultOptions, ctrl.options); + var elem = angular.element(element); + // Create new color pickr instance + var colorPicker = elem.spectrum(options); + colorPickerInstance = colorPicker; + if (colorPickerInstance) { + // destroy the color picker instance when the dom element is removed + elem.on('$destroy', function () { + colorPickerInstance.spectrum('destroy'); + }); + } + setUpCallbacks(); + // Refresh the scope + $scope.$applyAsync(); + } + // Spectrum events: https://seballot.github.io/spectrum/#events + function setUpCallbacks() { + if (colorPickerInstance) { + // bind hook for beforeShow + if (ctrl.onBeforeShow) { + colorPickerInstance.on('beforeShow.spectrum', function (e, tinycolor) { + $timeout(function () { + ctrl.onBeforeShow({ color: tinycolor }); + }); + }); + } + // bind hook for show + if (ctrl.onShow) { + colorPickerInstance.on('show.spectrum', function (e, tinycolor) { + $timeout(function () { + ctrl.onShow({ color: tinycolor }); + }); + }); + } + // bind hook for hide + if (ctrl.onHide) { + colorPickerInstance.on('hide.spectrum', function (e, tinycolor) { + $timeout(function () { + ctrl.onHide({ color: tinycolor }); + }); + }); + } + // bind hook for change + if (ctrl.onChange) { + colorPickerInstance.on('change.spectrum', function (e, tinycolor) { + $timeout(function () { + ctrl.onChange({ color: tinycolor }); + }); + }); + } + // bind hook for move + if (ctrl.onMove) { + colorPickerInstance.on('move.spectrum', function (e, tinycolor) { + $timeout(function () { + ctrl.onMove({ color: tinycolor }); + }); + }); + } + // bind hook for drag start + if (ctrl.onDragStart) { + colorPickerInstance.on('dragstart.spectrum', function (e, tinycolor) { + $timeout(function () { + ctrl.onDragStart({ color: tinycolor }); + }); + }); + } + // bind hook for drag stop + if (ctrl.onDragStop) { + colorPickerInstance.on('dragstop.spectrum', function (e, tinycolor) { + $timeout(function () { + ctrl.onDragStop({ color: tinycolor }); + }); + }); + } + } + } + } + angular.module('umbraco.directives').component('umbColorPicker', { + template: '
      ', + controller: ColorPickerController, + bindings: { + ngModel: '<', + options: '<', + onBeforeShow: '&?', + onShow: '&?', + onHide: '&?', + onChange: '&?', + onMove: '&?', + onDragStart: '&?', + onDragStop: '&?' + } + }); + }()); + 'use strict'; + /** +@ngdoc directive +@name umbraco.directives.directive:umbColorSwatches +@restrict E +@scope +@description +Use this directive to generate color swatches to pick from. +

      Markup example

      +
      +    
      +    
      +
      +@param {array} colors (attribute): The array of colors. +@param {string} selectedColor (attribute): The selected color. +@param {string} size (attribute): The size (s, m). +@param {string} useLabel (attribute): Specify if labels should be used. +@param {string} useColorClass (attribute): Specify if color values are css classes. +@param {string} colorClassNamePrefix (attribute): Specify the prefix used for the class for each color (defaults to "btn"). +@param {function} onSelect (expression): Callback function when the item is selected. +**/ + (function () { + 'use strict'; + function ColorSwatchesDirective() { + function link(scope, el, attr, ctrl) { + // Set default to true if not defined + if (Utilities.isUndefined(scope.useColorClass)) { + scope.useColorClass = false; + } + // Set default to "btn" if not defined + if (Utilities.isUndefined(scope.colorClassNamePrefix)) { + scope.colorClassNamePrefix = 'btn'; + } + scope.setColor = function (color, $index, $event) { + if (scope.onSelect) { + // did the value change? + if (scope.selectedColor != null && scope.selectedColor.value === color.value) { + // User clicked the currently selected color + // to remove the selection, they don't want + // to select any color after all. + // Unselect the color + color = null; + } + scope.selectedColor = color; scope.onSelect({ color: color, $index: $index, @@ -11031,7 +12494,7 @@ Use this directive to generate color swatches to pick from. restrict: 'E', replace: true, transclude: true, - template: '
      ', + template: '
      ', scope: { colors: '=?', size: '@', @@ -11102,7 +12565,7 @@ A confirmation dialog // restrict to an element replace: true, // replace the html element with the template - template: '

      {{caption}}

      ', + template: '

      {{caption}}

      ', scope: { onConfirm: '=', onCancel: '=', @@ -11149,7 +12612,7 @@ The prompt can be opened in four direction up, down, left or right.

      - + var directive = { restrict: 'E', replace: true, - template: '
      ', + template: '
      ', scope: { direction: '@', onConfirm: '&', @@ -11353,7 +12816,7 @@ Use this directive to generate a list of content items presented as a flexbox gr var directive = { restrict: 'E', replace: true, - template: '
      {{ item.name }}
      • :
      • {{ property.header }}:
        {{ item[property.alias] }}
      There are no items to show
      ', + template: '
      {{item.name}}
      • :
      • {{ property.header }}:
        {{ item[property.alias] }}
      There are no items to show
      ', scope: { content: '=', contentProperties: '=', @@ -11505,7 +12968,7 @@ Use this directive to render a date time picker fpInstance.setDate(ctrl.ngModel); } // destroy the flatpickr instance when the dom element is removed - angular.element(element).on('$destroy', function () { + $(element).on('$destroy', function () { fpInstance.destroy(); }); // Refresh the scope @@ -11656,7 +13119,7 @@ Use this directive to render a date time picker - {{ item.name }} + @@ -11848,7 +13311,7 @@ Use this directive to render a file icon. } } var component = { - template: ' .{{vm.extension}} {{vm.text}} ', + template: ' .{{vm.extension}} {{vm.text}} ', bindings: { extension: '@?', icon: '@?', @@ -11960,7 +13423,7 @@ Use this directive to generate a list of folders presented as a flexbox grid. var directive = { restrict: 'E', replace: true, - template: '
      {{ folder.name }}
      ', + template: '
      {{folder.name}}
      ', scope: { folders: '=', onClick: '=', @@ -12025,7 +13488,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc angular.module('umbraco.directives').directive('umbGenerateAlias', function ($timeout, entityResource, localizationService) { return { restrict: 'E', - template: '
      {{ alias }}
      ', + template: '
      {{ alias }}
      ', replace: true, scope: { alias: '=', @@ -12176,7 +13639,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc } } // update selected items - angular.forEach(scope.selectedItems, function (selectedItem) { + Utilities.forEach(scope.selectedItems, function (selectedItem) { if (selectedItem.placeholder) { selectedItem.name = scope.name; if (scope.alias !== null && scope.alias !== undefined) { @@ -12185,7 +13648,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc } }); // update availableItems - angular.forEach(scope.availableItems, function (availableItem) { + Utilities.forEach(scope.availableItems, function (availableItem) { if (availableItem.placeholder) { availableItem.name = scope.name; if (scope.alias !== null && scope.alias !== undefined) { @@ -12217,7 +13680,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc var directive = { restrict: 'E', replace: true, - template: '
      {{ defaultItem.name }}
      (Default {{itemLabel}})
      Remove Template
      {{ selectedItem.name }}
      Remove Template
      All {{itemLabel}}s are added
      ', + template: '
      {{ defaultItem.name }}
      (Default {{itemLabel}})
      {{ selectedItem.name }}
      All {{itemLabel}}s are added
      ', scope: { name: '=', alias: '=', @@ -12234,108 +13697,304 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc angular.module('umbraco.directives').directive('umbGridSelector', GridSelector); }()); 'use strict'; + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); + } + function _nonIterableSpread() { + throw new TypeError('Invalid attempt to spread non-iterable instance'); + } + function _iterableToArray(iter) { + if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === '[object Arguments]') + return Array.from(iter); + } + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } + } + function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + if (enumerableOnly) + symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + }); + keys.push.apply(keys, symbols); + } + return keys; + } + function _objectSpread(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {}; + if (i % 2) { + ownKeys(source, true).forEach(function (key) { + _defineProperty(target, key, source[key]); + }); + } else if (Object.getOwnPropertyDescriptors) { + Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + } else { + ownKeys(source).forEach(function (key) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + }); + } + } + return target; + } + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + return obj; + } (function () { 'use strict'; - function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService, editorService, eventsService, overlayService) { - function link(scope, el, attr, ctrl) { + function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService, editorService, eventsService, overlayService) { + function link(scope, element) { + var TYPE_GROUP = contentTypeHelper.TYPE_GROUP; + var TYPE_TAB = contentTypeHelper.TYPE_TAB; var eventBindings = []; var validationTranslated = ''; var tabNoSortOrderTranslated = ''; scope.dataTypeHasChanged = false; scope.sortingMode = false; scope.toolbar = []; - scope.sortableOptionsGroup = {}; - scope.sortableOptionsProperty = {}; scope.sortingButtonKey = 'general_reorder'; scope.compositionsButtonState = 'init'; + scope.tabs = []; + scope.genericGroups = []; + scope.openTabAlias = null; + scope.hasGenericTab = false; + scope.genericTab = { + key: String.CreateGuid(), + type: TYPE_TAB, + name: 'Generic', + alias: null, + parentAlias: null, + sortOrder: 0, + properties: [] + }; + var tabsInitialized = false; + eventBindings.push(scope.$watchCollection('model.groups', function (newValue, oldValue) { + // we only want to run this logic when new groups are added or removed + if (newValue.length === oldValue.length && tabsInitialized) { + tabsInitialized = true; + return; + } + contentTypeHelper.defineParentAliasOnGroups(newValue); + contentTypeHelper.relocateDisorientedGroups(newValue); + scope.tabs = $filter('filter')(newValue, function (group) { + return group.type === TYPE_TAB && group.parentAlias == null; + }); + // order tabs + scope.orderTabs(); + // set server validation index + // the server filters out inherited groups if they don't have any local properties when returning the group index + var noInherited = newValue.filter(function (group) { + return !group.inherited || group.inherited && group.properties.filter(function (property) { + return !property.inherited; + }).length > 0; + }); + noInherited.forEach(function (group, index) { + group.serverValidationIndex = !group.inherited ? index : undefined; + }); + checkGenericTabVisibility(); + if (!scope.openTabAlias && scope.hasGenericTab) { + scope.openTabAlias = null; + } else if (!scope.openTabAlias && scope.tabs.length > 0) { + scope.openTabAlias = scope.tabs[0].alias; + } + tabsInitialized = true; + })); 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; + localizationService.localizeMany([ + 'validation_validation', + 'contentTypeEditor_tabHasNoSortOrder', + 'general_generic', + 'contentTypeEditor_tabDirectPropertiesDropZone' + ]).then(function (data) { + validationTranslated = data[0]; + tabNoSortOrderTranslated = data[1]; + scope.genericTab.name = data[2]; + scope.tabDirectPropertiesDropZoneLabel = data[3]; }); } function setSortingOptions() { - scope.sortableOptionsGroup = { - axis: 'y', - distance: 10, + var defaultOptions = { + axis: '', tolerance: 'pointer', opacity: 0.7, scroll: true, cursor: 'move', - placeholder: 'umb-group-builder__group-sortable-placeholder', zIndex: 6000, + forcePlaceholderSize: true, + dropOnEmpty: true, + helper: 'clone', + appendTo: 'body' + }; + scope.sortableOptionsTab = _objectSpread(_objectSpread({}, defaultOptions), {}, { + connectWith: '.umb-group-builder__tabs', + placeholder: 'umb-group-builder__tab-sortable-placeholder', + handle: '.umb-group-builder__tab-handle', + items: '.umb-group-builder__tab-sortable', + stop: function stop(event, ui) { + var tabKey = ui.item[0].dataset.tabKey ? ui.item[0].dataset.tabKey : false; + var dropIndex = scope.tabs.findIndex(function (tab) { + return tab.key === tabKey; + }); + updateSortOrder(scope.tabs, dropIndex); + } + }); + scope.sortableOptionsGroup = _objectSpread(_objectSpread({}, defaultOptions), {}, { + connectWith: '.umb-group-builder__groups', + placeholder: 'umb-group-builder__group-sortable-placeholder', handle: '.umb-group-builder__group-handle', items: '.umb-group-builder__group-sortable', - start: function start(e, ui) { - ui.placeholder.height(ui.item.height()); - }, stop: function stop(e, ui) { - updateTabsSortOrder(); + var groupKey = ui.item[0].dataset.groupKey ? ui.item[0].dataset.groupKey : false; + var group = groupKey ? scope.model.groups.find(function (group) { + return group.key === groupKey; + }) : {}; + // the code also runs when you convert a group to a tab. + // We want to make sure it only run when groups are reordered + if (group && group.type === TYPE_GROUP) { + // Update aliases + var parentAlias = scope.openTabAlias; + var oldAlias = group.alias || null; + // null when group comes from root aka. 'generic' + var newAlias = contentTypeHelper.updateParentAlias(oldAlias, parentAlias); + group.alias = newAlias; + group.parentAlias = parentAlias; + contentTypeHelper.updateDescendingAliases(scope.model.groups, oldAlias, newAlias); + var groupsInTab = scope.model.groups.filter(function (group) { + return group.parentAlias === parentAlias; + }); + var dropIndex = groupsInTab.findIndex(function (group) { + return group.key === groupKey; + }); + updateSortOrder(groupsInTab, dropIndex); + // when a group is dropped we need to reset the requested tab hover alias + scope.sortableRequestedTabAlias = undefined; + } } - }; - scope.sortableOptionsProperty = { - axis: 'y', - distance: 10, - tolerance: 'pointer', + }); + scope.sortableOptionsProperty = _objectSpread(_objectSpread({}, defaultOptions), {}, { 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 start(e, ui) { - ui.placeholder.height(ui.item.height()); - }, stop: function stop(e, ui) { updatePropertiesSortOrder(); } + }); + scope.droppableOptionsConvert = { + accept: '.umb-group-builder__group-sortable', + tolerance: 'pointer', + drop: function drop(evt, ui) { + var groupKey = ui.draggable[0].dataset.groupKey ? ui.draggable[0].dataset.groupKey : false; + var group = groupKey ? scope.model.groups.find(function (group) { + return group.key === groupKey; + }) : {}; + if (group) { + contentTypeHelper.convertGroupToTab(scope.model.groups, group); + scope.tabs.push(group); + scope.$broadcast('umbOverflowChecker.checkOverflow'); + scope.$broadcast('umbOverflowChecker.scrollTo', { position: 'end' }); + } + } }; - } - 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; + scope.sortableRequestedTabAlias = undefined; + //set to undefined as null is the generic group. + scope.sortableRequestedTabTimeout = null; + scope.droppableOptionsTab = { + accept: '.umb-group-builder__property-sortable, .umb-group-builder__group-sortable', + tolerance: 'pointer', + over: function over(evt, ui) { + var hoveredTabAlias = evt.target.dataset.tabAlias === '' ? null : evt.target.dataset.tabAlias; + // if dragging a group + if (ui.draggable[0].dataset.groupKey) { + var groupKey = ui.draggable[0].dataset.groupKey ? ui.draggable[0].dataset.groupKey : false; + var group = groupKey ? scope.model.groups.find(function (group) { + return group.key === groupKey; + }) : {}; + var newAlias = contentTypeHelper.updateParentAlias(group.alias || null, hoveredTabAlias); + // Check alias is unique + if (group.alias !== newAlias && contentTypeHelper.isAliasUnique(scope.model.groups, newAlias) === false) { + localizationService.localize('contentTypeEditor_groupReorderSameAliasError', [ + group.name, + newAlias + ]).then(function (value) { + notificationsService.error(value); + }); + return; } - 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; + } + if (scope.sortableRequestedTabAlias !== hoveredTabAlias) { + if (scope.sortableRequestedTabTimeout !== null) { + $timeout.cancel(scope.sortableRequestedTabTimeout); + scope.sortableRequestedTabTimeout = null; + scope.sortableRequestedTabAlias = undefined; } + scope.sortableRequestedTabAlias = hoveredTabAlias; + scope.sortableRequestedTabTimeout = $timeout(function () { + scope.openTabAlias = scope.sortableRequestedTabAlias; + scope.sortableRequestedTabTimeout = null; + /* hack to update sortable positions when switching from one tab to another. + without this sorting direct properties doesn't work correctly */ + scope.$apply(); + $('.umb-group-builder__ungrouped-properties .umb-group-builder__properties').sortable('refresh'); + $('.umb-group-builder__groups').sortable('refresh'); + }, 400); + } + }, + out: function out(evt, ui) { + var hoveredTabAlias = evt.target.dataset.tabAlias === '' ? null : evt.target.dataset.tabAlias; + if (scope.sortableRequestedTabTimeout !== null && (hoveredTabAlias === undefined || scope.sortableRequestedTabAlias === hoveredTabAlias)) { + $timeout.cancel(scope.sortableRequestedTabTimeout); + scope.sortableRequestedTabTimeout = null; + scope.sortableRequestedTabAlias = null; } - // store this tabs sort order as reference for the next - prevSortOrder = group.sortOrder; } - }); + }; + } + function updateSortOrder(items, movedIndex) { + if (items && items.length <= 1) { + return; + } + // update the moved item sort order to fit into where it is dragged + var movedItem = items[movedIndex]; + if (movedIndex === 0) { + var nextItem = items[movedIndex + 1]; + movedItem.sortOrder = nextItem.sortOrder - 1; + } else { + var prevItem = items[movedIndex - 1]; + movedItem.sortOrder = prevItem.sortOrder + 1; + } + /* After the above two items next to each other might have the same sort order + to prevent this we run through the rest of the + items and update the sort order if they are next to each other. + This will make it possible to make gaps without the number being updated */ + for (var i = movedIndex; i < items.length; i++) { + var item = items[i]; + if (!item.inherited && i !== 0) { + var prev = items[i - 1]; + if (item.sortOrder === prev.sortOrder) { + item.sortOrder = item.sortOrder + 1; + } + } + } } function filterAvailableCompositions(selectedContentType, selecting) { //selecting = true if the user has check the item, false if the user has unchecked the item @@ -12355,38 +14014,36 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc //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) { + scope.compositionsDialogModel.availableCompositeContentTypes.forEach(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) { + var found = filteredAvailableCompositeTypes.find(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); + current.allowed = scope.model.lockedCompositeContentTypes.includes(current.contentType.alias) && selectedContentTypeAliases.includes(current.contentType.alias) || (found ? found.allowed : false); }); }); } function updatePropertiesSortOrder() { - angular.forEach(scope.model.groups, function (group) { - if (group.tabState !== 'init') { - group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties); - } + scope.model.groups.forEach(function (group) { + return 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) { + scope.compositionsDialogModel.availableCompositeContentTypes.forEach(function (c) { //enable it if it's part of the selected model - if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) { + if (scope.compositionsDialogModel.compositeContentTypes.includes(c.contentType.alias)) { c.allowed = true; } //set the inherited flags c.inherited = false; - if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) { + if (scope.model.lockedCompositeContentTypes.includes(c.contentType.alias)) { c.inherited = true; } // convert icons for composite content types @@ -12401,7 +14058,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc object.deletePrompt = false; }; /* ---------- TOOLBAR ---------- */ - scope.toggleSortingMode = function (tool) { + scope.toggleSortingMode = function () { if (scope.sortingMode === true) { var sortOrderMissing = false; for (var i = 0; i < scope.model.groups.length; i++) { @@ -12416,10 +14073,16 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc scope.sortingMode = false; scope.sortingButtonKey = 'general_reorder'; } + // When exiting the reorder mode while the generic tab is empty, set the active tab to the first available one + if (scope.tabs.length > 0 && !scope.openTabAlias) { + scope.openTabAlias = scope.tabs[0].alias; + } } else { scope.sortingMode = true; scope.sortingButtonKey = 'general_reorderDone'; } + checkGenericTabVisibility(); + scope.$broadcast('umbOverflowChecker.checkOverflow'); }; scope.openCompositionsDialog = function () { scope.compositionsDialogModel = { @@ -12428,23 +14091,16 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc view: 'views/common/infiniteeditors/compositions/compositions.html', size: 'small', submit: function submit() { - // 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 editorService.close(); }, close: function close(oldModel) { // reset composition changes scope.model.groups = oldModel.contentType.groups; scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes; - // remove overlay editorService.close(); }, selectCompositeContentType: function selectCompositeContentType(selectedContentType) { + var deferred = $q.defer(); //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; @@ -12466,6 +14122,12 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc } //based on the selection, we need to filter the available composite types list filterAvailableCompositions(selectedContentType, newSelection).then(function () { + deferred.resolve({ + selectedContentType: selectedContentType, + newSelection: newSelection + }); // TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }, function () { + deferred.reject(); }); }); } else { @@ -12473,8 +14135,15 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); //based on the selection, we need to filter the available composite types list filterAvailableCompositions(selectedContentType, newSelection).then(function () { + deferred.resolve({ + selectedContentType: selectedContentType, + newSelection: newSelection + }); // TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }, function () { + deferred.reject(); }); } + return deferred.promise; } }; //select which resource methods to use, eg document Type or Media Type versions @@ -12497,7 +14166,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc }), //get where used document types whereUsedContentTypeResource(scope.model.id).then(function (whereUsed) { - //pass to the dialog model the content type eg documentType or mediaType + //pass to the dialog model the content type eg documentType or mediaType scope.compositionsDialogModel.section = scope.contentType; //pass the list of 'where used' document types scope.compositionsDialogModel.whereCompositionUsed = whereUsed; @@ -12515,7 +14184,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc scope.openDocumentType = function (documentTypeId) { var editor = { id: documentTypeId, - submit: function submit(model) { + submit: function submit() { var args = { node: scope.model }; eventsService.emit('editors.documentType.reload', args); editorService.close(); @@ -12526,26 +14195,210 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc }; editorService.documentTypeEditor(editor); }; - /* ---------- 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; + /* ---------- TABS ---------- */ + scope.changeTab = function (_ref) { + var alias = _ref.alias; + scope.openTabAlias = alias; + }; + scope.addTab = function () { + var newTabIndex = scope.tabs.length; + var lastTab = scope.tabs[newTabIndex - 1]; + var sortOrder = lastTab && lastTab.sortOrder !== undefined ? lastTab.sortOrder + 1 : 0; + var key = String.CreateGuid(); + var tab = { + key: key, + type: TYPE_TAB, + name: '', + alias: key, + // Temporarily set alias to key, because the name is empty + parentAlias: null, + sortOrder: sortOrder, + properties: [] + }; + if (newTabIndex === 0 && scope.hasGenericTab === false) { + scope.model.groups.forEach(function (group) { + if (!group.inherited && group.parentAlias == null) { + group.parentAlias = tab.alias; + group.alias = contentTypeHelper.updateParentAlias(group.alias, group.parentAlias); + } + }); + } + scope.model.groups = [].concat(_toConsumableArray(scope.model.groups), [tab]); + scope.openTabAlias = tab.alias; + notifyChanged(); + scope.$broadcast('umbOverflowChecker.checkOverflow'); + scope.$broadcast('umbOverflowChecker.scrollTo', { position: 'end' }); + }; + scope.removeTab = function (tab, indexInTabs) { + var tabName = tab.name || ''; + var localizeMany = localizationService.localizeMany([ + 'general_delete', + 'contentTypeEditor_confirmDeleteTabNotice' + ]); + var localize = localizationService.localize('contentTypeEditor_confirmDeleteTabMessage', [tabName]); + $q.all([ + localizeMany, + localize + ]).then(function (values) { + var translations = values[0]; + var message = values[1]; + overlayService.confirmDelete({ + title: ''.concat(translations[0], ' ').concat(tabName), + content: message, + confirmMessage: translations[1], + submitButtonLabelKey: 'actions_delete', + submit: function submit() { + var indexInGroups = scope.model.groups.findIndex(function (group) { + return group.alias === tab.alias; + }); + scope.model.groups.splice(indexInGroups, 1); + // remove all child groups + scope.model.groups = scope.model.groups.filter(function (group) { + return group.parentAlias !== tab.alias; + }); + // we need a timeout because the filter hasn't updated the tabs collection + $timeout(function () { + if (scope.tabs.length > 0) { + scope.openTabAlias = indexInTabs > 0 ? scope.tabs[indexInTabs - 1].alias : scope.tabs[0].alias; + } else { + scope.openTabAlias = null; + } + }); + scope.$broadcast('umbOverflowChecker.checkOverflow'); + notifyChanged(); + overlayService.close(); + } + }); + }); + }; + scope.canRemoveTab = function (tab) { + return tab.inherited !== true; + }; + scope.setTabOverflowState = function (overflowLeft, overflowRight) { + scope.overflow = { + left: overflowLeft, + right: overflowRight + }; + }; + scope.moveTabsOverflowLeft = function () { + //TODO: optimize this... + var el = element[0].querySelector('.umb-group-builder__tabs-list'); + el.scrollLeft -= el.clientWidth * 0.5; + }; + scope.moveTabsOverflowRight = function () { + //TODO: optimize this... + var el = element[0].querySelector('.umb-group-builder__tabs-list'); + el.scrollLeft += el.clientWidth * 0.5; + }; + scope.orderTabs = function () { + scope.tabs = $filter('orderBy')(scope.tabs, 'sortOrder'); + }; + scope.onChangeTabName = function (tab) { + if (updateGroupAlias(tab)) { + scope.openTabAlias = tab.alias; + scope.$broadcast('umbOverflowChecker.checkOverflow'); + } + }; + /** Universal method for updating group alias (for tabs, field-sets etc.) */ + function updateGroupAlias(group) { + var localAlias = contentTypeHelper.generateLocalAlias(group.name), oldAlias = group.alias; + var newAlias = contentTypeHelper.updateLocalAlias(oldAlias, localAlias); + // Ensure unique alias, otherwise we would be transforming groups of other parents, we do not want this. + if (contentTypeHelper.isAliasUnique(scope.model.groups, newAlias) === false) { + newAlias = contentTypeHelper.createUniqueAlias(scope.model.groups, newAlias); + } + group.alias = newAlias; + group.parentAlias = contentTypeHelper.getParentAlias(newAlias); + contentTypeHelper.updateDescendingAliases(scope.model.groups, oldAlias, newAlias); + return true; + } + scope.isUngroupedPropertiesVisible = function (_ref2) { + var alias = _ref2.alias, properties = _ref2.properties; + var isOpenTab = alias === scope.openTabAlias; + if (isOpenTab && properties.length > 0) { + return true; + } + if (isOpenTab && scope.sortingMode) { + return true; + } + var tabHasGroups = scope.model.groups.filter(function (group) { + return group.parentAlias === alias; + }).length > 0; + if (isOpenTab && !tabHasGroups) { + return true; } - // activate group + }; + function checkGenericTabVisibility() { + var hasRootGroups = scope.model.groups.filter(function (group) { + return group.type === TYPE_GROUP && group.parentAlias === null; + }).length > 0; + scope.hasGenericTab = hasRootGroups && scope.tabs.length > 0 || scope.sortingMode; + } + /* Properties */ + scope.addNewProperty = function (group) { + var newProperty = { + label: null, + alias: null, + propertyState: 'init', + validation: { + mandatory: false, + mandatoryMessage: null, + pattern: null, + patternMessage: null + }, + labelOnTop: false + }; + var propertySettings = { + title: 'Property settings', + property: newProperty, + contentType: scope.contentType, + contentTypeName: scope.model.name, + contentTypeAllowCultureVariant: scope.model.allowCultureVariant, + contentTypeAllowSegmentVariant: scope.model.allowSegmentVariant, + view: 'views/common/infiniteeditors/propertysettings/propertysettings.html', + size: 'small', + submit: function submit(model) { + newProperty = _objectSpread({}, model.property); + newProperty.propertyState = 'active'; + group.properties.push(newProperty); + editorService.close(); + }, + close: function close() { + editorService.close(); + } + }; + editorService.open(propertySettings); + }; + /* ---------- GROUPS ---------- */ + scope.addGroup = function (tabAlias) { + scope.model.groups = scope.model.groups || []; + var groupsInTab = scope.model.groups.filter(function (group) { + return group.parentAlias === tabAlias; + }); + var lastGroupSortOrder = groupsInTab.length > 0 ? groupsInTab[groupsInTab.length - 1].sortOrder + 1 : 0; + var key = String.CreateGuid(); + var group = { + key: key, + type: TYPE_GROUP, + name: '', + alias: contentTypeHelper.updateParentAlias(key, tabAlias), + // Temporarily set alias to key, because the name is empty + parentAlias: tabAlias || null, + sortOrder: lastGroupSortOrder, + properties: [], + parentTabContentTypes: [], + parentTabContentTypeNames: [] + }; + scope.model.groups = [].concat(_toConsumableArray(scope.model.groups), [group]); scope.activateGroup(group); - // push new init tab to the scope - addInitGroup(scope.model.groups); + notifyChanged(); }; scope.activateGroup = function (selectedGroup) { + if (!selectedGroup) { + return; + } // set all other groups that are inactive to active - angular.forEach(scope.model.groups, function (group) { + scope.model.groups.forEach(function (group) { // skip init tab if (group.tabState !== 'init') { group.tabState = 'inActive'; @@ -12553,18 +14406,45 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc }); selectedGroup.tabState = 'active'; }; + scope.onChangeGroupName = function (group) { + updateGroupAlias(group); + }; scope.canRemoveGroup = function (group) { return group.inherited !== true && _.find(group.properties, function (property) { return property.locked === true; }) == null; }; - scope.removeGroup = function (groupIndex) { - scope.model.groups.splice(groupIndex, 1); + scope.removeGroup = function (selectedGroup) { + var groupName = selectedGroup.name || ''; + var localizeMany = localizationService.localizeMany([ + 'general_delete', + 'contentTypeEditor_confirmDeleteGroupNotice' + ]); + var localize = localizationService.localize('contentTypeEditor_confirmDeleteGroupMessage', [groupName]); + $q.all([ + localizeMany, + localize + ]).then(function (values) { + var translations = values[0]; + var message = values[1]; + overlayService.confirmDelete({ + title: ''.concat(translations[0], ' ').concat(groupName), + content: message, + confirmMessage: translations[1], + submitButtonLabelKey: 'actions_delete', + submit: function submit() { + var index = scope.model.groups.findIndex(function (group) { + return group.alias === selectedGroup.alias; + }); + scope.model.groups.splice(index, 1); + overlayService.close(); + notifyChanged(); + } + }); + }); }; - scope.updateGroupTitle = function (group) { - if (group.properties.length === 0) { - addInitProperty(group); - } + scope.addGroupToActiveTab = function () { + scope.addGroup(scope.openTabAlias); }; scope.changeSortOrderValue = function (group) { if (group.sortOrder !== undefined) { @@ -12572,51 +14452,25 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc } 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; - } + scope.onChangeGroupSortOrderValue = function (sortedGroup) { + var groupsInTab = scope.model.groups.filter(function (group) { + return group.parentAlias === sortedGroup.parentAlias; }); - 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'; - } - } - } + var otherGroups = scope.model.groups.filter(function (group) { + return group.parentAlias !== sortedGroup.parentAlias; + }); + var sortedGroups = $filter('orderBy')(groupsInTab, 'sortOrder'); + scope.model.groups = [].concat(_toConsumableArray(otherGroups), _toConsumableArray(sortedGroups)); + }; /* ---------- PROPERTIES ---------- */ scope.addPropertyToActiveGroup = function () { - var group = _.find(scope.model.groups, function (group) { + var activeGroup = scope.model.groups.find(function (group) { return group.tabState === 'active'; }); - if (!group && scope.model.groups.length) { - group = scope.model.groups[0]; - } - if (!group || !group.name) { - return; - } - var property = _.find(group.properties, function (property) { - return property.propertyState === 'init'; - }); - if (!property) { - return; + if (!activeGroup && scope.model.groups.length) { + activeGroup = scope.model.groups[0]; } - scope.addProperty(property, group); + scope.addNewProperty(activeGroup); }; scope.addProperty = function (property, group) { // set property sort order @@ -12634,19 +14488,20 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc }; scope.editPropertyTypeSettings = function (property, group) { if (!property.inherited) { - var oldPropertyModel = angular.copy(property); + var oldPropertyModel = Utilities.copy(property); if (oldPropertyModel.allowCultureVariant === undefined) { // this is necessary for comparison when detecting changes to the property oldPropertyModel.allowCultureVariant = scope.model.allowCultureVariant; oldPropertyModel.alias = ''; } - var propertyModel = angular.copy(property); + var propertyModel = Utilities.copy(property); var propertySettings = { title: 'Property settings', property: propertyModel, contentType: scope.contentType, contentTypeName: scope.model.name, contentTypeAllowCultureVariant: scope.model.allowCultureVariant, + contentTypeAllowSegmentVariant: scope.model.allowSegmentVariant, view: 'views/common/infiniteeditors/propertysettings/propertysettings.html', size: 'small', submit: function submit(model) { @@ -12672,17 +14527,21 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc property.isSensitiveData = propertyModel.isSensitiveData; property.isSensitiveValue = propertyModel.isSensitiveValue; property.allowCultureVariant = propertyModel.allowCultureVariant; + property.allowSegmentVariant = propertyModel.allowSegmentVariant; + property.labelOnTop = propertyModel.labelOnTop; // update existing data types if (model.updateSameDataTypes) { updateSameDataTypes(property); } // close the editor editorService.close(); - // push new init property to group - addInitProperty(group); - // set focus on init property - var numberOfProperties = group.properties.length; - group.properties[numberOfProperties - 1].focus = true; + if (group) { + // push new init property to group + addInitProperty(group); + // set focus on init property + var numberOfProperties = group.properties.length; + group.properties[numberOfProperties - 1].focus = true; + } notifyChanged(); }, close: function close() { @@ -12723,10 +14582,34 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc property.dialogIsOpen = true; } }; - scope.deleteProperty = function (tab, propertyIndex) { - // remove property - tab.properties.splice(propertyIndex, 1); - notifyChanged(); + scope.deleteProperty = function (properties, _ref3) { + var id = _ref3.id, label = _ref3.label; + var propertyName = label || ''; + var localizeMany = localizationService.localizeMany(['general_delete']); + var localize = localizationService.localize('contentTypeEditor_confirmDeletePropertyMessage', [propertyName]); + $q.all([ + localizeMany, + localize + ]).then(function (values) { + var translations = values[0]; + var message = values[1]; + overlayService.confirmDelete({ + title: ''.concat(translations[0], ' ').concat(propertyName), + content: message, + submitButtonLabelKey: 'actions_delete', + submit: function submit() { + var index = properties.findIndex(function (property) { + return property.id === id; + }); + properties.splice(index, 1); + notifyChanged(); + overlayService.close(); + } + }); + }); + }; + scope.onChangePropertySortOrderValue = function (group) { + group.properties = $filter('orderBy')(group.properties, 'sortOrder'); }; function notifyChanged() { eventsService.emit('editors.groupsBuilder.changed'); @@ -12742,10 +14625,11 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc mandatoryMessage: null, pattern: null, patternMessage: null - } + }, + labelOnTop: false }; // check if there already is an init property - angular.forEach(group.properties, function (property) { + Utilities.forEach(group.properties, function (property) { if (property.propertyState === 'init') { addInitPropertyBool = false; } @@ -12757,8 +14641,8 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc } function updateSameDataTypes(newProperty) { // find each property - angular.forEach(scope.model.groups, function (group) { - angular.forEach(group.properties, function (property) { + Utilities.forEach(scope.model.groups, function (group) { + Utilities.forEach(group.properties, function (property) { if (property.dataTypeId === newProperty.dataTypeId) { // update property data property.config = newProperty.config; @@ -12806,7 +14690,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc var directive = { restrict: 'E', replace: true, - template: '
      • {{groupNameForm.groupName.errorMsg}}
        : {{ tab.inheritedFromName }} ,
        • {{ property.alias }}
          {{propertyTypeForm.groupName.errorMsg}}
          {{ property.label }} ({{ property.alias }})
          {{property.dataTypeName}}
          *
          {{property.contentTypeName}}

      ', + template: '
      {{ tabDirectPropertiesDropZoneLabel }}
      ', scope: { model: '=', compositions: '=', @@ -12822,6 +14706,84 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc 'use strict'; /** @ngdoc directive +@name umbraco.directives.directive:umbIcon +@restrict E +@scope +@description +Use this directive to show an render an umbraco backoffice svg icon. All svg icons used by this directive should use the following naming convention to keep things consistent: icon-[name of icon]. For example
      icon-alert.svg
      + +

      Markup example

      + +Simple icon +
      +    
      +
      + +Icon with additional attribute. It can be treated like any other dom element +
      +    
      +
      +@example + **/ + (function () { + 'use strict'; + function UmbIconDirective(iconHelper) { + var directive = { + replace: true, + transclude: true, + template: ' ', + scope: { + icon: '@', + svgString: '=?' + }, + link: function link(scope, element) { + if (scope.svgString === undefined && scope.svgString !== null && scope.icon !== undefined && scope.icon !== null) { + var observer = new IntersectionObserver(_lazyRequestIcon, { rootMargin: '100px' }); + var iconEl = element[0]; + observer.observe(iconEl); + // make sure to disconnect the observer when the scope is destroyed + scope.$on('$destroy', function () { + observer.disconnect(); + }); + } + scope.$watch('icon', function (newValue, oldValue) { + if (newValue && oldValue) { + var newicon = newValue.split(' ')[0]; + var oldicon = oldValue.split(' ')[0]; + if (newicon !== oldicon) { + _requestIcon(newicon); + } + } + }); + function _lazyRequestIcon(entries, observer) { + entries.forEach(function (entry) { + if (entry.isIntersecting === true) { + observer.disconnect(); + var icon = scope.icon.split(' ')[0]; + // Ensure that only the first part of the icon is used as sometimes the color is added too, e.g. see umbeditorheader.directive scope.openIconPicker + _requestIcon(icon); + } + }); + } + function _requestIcon(icon) { + // Reset svg string before requesting new icon. + scope.svgString = null; + iconHelper.getIcon(icon).then(function (data) { + if (data && data.svgString) { + // Watch source SVG string + scope.svgString = data.svgString; + } + }); + } + } + }; + return directive; + } + angular.module('umbraco.directives').directive('umbIcon', UmbIconDirective); + }()); + 'use strict'; + /** +@ngdoc directive @name umbraco.directives.directive:umbImageLazyLoad @restrict E @scope @@ -12858,7 +14820,7 @@ Use this directive to lazy-load an image only when it is scrolled into view. var placeholder = 'assets/img/transparent.png'; function link(scope, element, attrs) { var observer = new IntersectionObserver(loadImg); - var img = angular.element(element)[0]; + var img = element[0]; img.src = placeholder; observer.observe(img); function loadImg(changes) { @@ -13031,8 +14993,8 @@ When this combination is hit an overview is opened with shortcuts based on the m } }; function onInit() { - angular.forEach(scope.model, function (shortcutGroup) { - angular.forEach(shortcutGroup.shortcuts, function (shortcut) { + Utilities.forEach(scope.model, function (shortcutGroup) { + Utilities.forEach(shortcutGroup.shortcuts, function (shortcut) { shortcut.platformKeys = []; // get shortcut keys for mac if (isMac && shortcut.keys && shortcut.keys.mac) { @@ -13090,7 +15052,7 @@ When this combination is hit an overview is opened with shortcuts based on the m (function () { 'use strict'; angular.module('umbraco.directives').component('umbLayoutSelector', { - template: '
      ', + template: '
      ', controller: LayoutSelectorController, controllerAs: 'vm', bindings: { @@ -13171,9 +15133,9 @@ When this combination is hit an overview is opened with shortcuts based on the m ', + template: '
      ', scope: { items: '=', onClose: '=', @@ -13321,15 +15283,23 @@ When this combination is hit an overview is opened with shortcuts based on the m function link(scope) { scope.dataType = {}; scope.customListViewCreated = false; + var listViewPrefix = 'List View - '; var checkForCustomListView = function checkForCustomListView() { - return scope.dataType.name === 'List View - ' + scope.modelAlias; + return invariantEquals(scope.dataType.name, listViewPrefix + scope.modelAlias); }; + // We also use "localeCompare" a few other places. Should probably be moved to a utility/helper function in future. + function invariantEquals(a, b) { + return typeof a === 'string' && typeof b === 'string' ? a.localeCompare(b, undefined, { sensitivity: 'base' }) === 0 : a === b; + } /* ---------- INIT ---------- */ + var setDataType = function setDataType(dataType) { + scope.dataType = dataType; + listViewPrevalueHelper.setPrevalues(dataType.preValues); + }; var activate = function activate() { if (scope.enableListView) { dataTypeResource.getByName(scope.listViewName).then(function (dataType) { - scope.dataType = dataType; - listViewPrevalueHelper.setPrevalues(dataType.preValues); + setDataType(dataType); scope.customListViewCreated = checkForCustomListView(); }); } else { @@ -13364,7 +15334,7 @@ When this combination is hit an overview is opened with shortcuts based on the m scope.loading = true; dataTypeResource.createCustomListView(scope.modelAlias).then(function (dataType) { // store data type - scope.dataType = dataType; + setDataType(dataType); // set list view name on scope scope.listViewName = dataType.name; // change state to custom list view @@ -13383,7 +15353,7 @@ When this combination is hit an overview is opened with shortcuts based on the m // get default data type dataTypeResource.getByName(scope.listViewName).then(function (defaultDataType) { // store data type - scope.dataType = defaultDataType; + setDataType(defaultDataType); // change state to default list view scope.customListViewCreated = false; scope.loading = false; @@ -13410,7 +15380,7 @@ When this combination is hit an overview is opened with shortcuts based on the m var directive = { restrict: 'E', replace: true, - template: '
      {{ dataType.name }} (default)
      ', + template: '
      {{dataType.name}} (default)
      ', scope: { enableListView: '=', listViewName: '=', @@ -13631,7 +15601,7 @@ Use this directive to render a value with a lock next to it. When the lock is cl require: 'ngModel', restrict: 'E', replace: true, - template: '
      Required alias
      Invalid alias
      {{lockedFieldForm.lockedField.errorMsg}}
      ', + template: '
      Required alias
      Invalid alias
      {{lockedFieldForm.lockedField.errorMsg}}
      ', scope: { ngModel: '=', locked: '=?', @@ -13892,6 +15862,9 @@ Use this directive to generate a thumbnail grid of media items. } } scope.clickItem = function (item, $event, $index) { + if (item.isFolder === true && item.filtered) { + scope.clickItemName(item, $event, $index); + } if (scope.onClick) { scope.onClick(item, $event, $index); $event.stopPropagation(); @@ -13909,18 +15882,54 @@ Use this directive to generate a thumbnail grid of media items. } }; var unbindItemsWatcher = scope.$watch('items', function (newValue, oldValue) { - if (angular.isArray(newValue)) { + if (Utilities.isArray(newValue)) { activate(); } }); scope.$on('$destroy', function () { unbindItemsWatcher(); }); + //determine if sort is current + scope.sortColumn = 'name'; + scope.sortReverse = false; + scope.sortDirection = 'asc'; + //check sort status + scope.isSortDirection = function (col, direction) { + return col === scope.sortColumn && direction === scope.sortDirection; + }; + //change sort + scope.setSort = function (col) { + if (scope.sortColumn === col) { + scope.sortReverse = !scope.sortReverse; + } else { + scope.sortColumn = col; + if (col === 'updateDate') { + scope.sortReverse = true; + } else { + scope.sortReverse = false; + } + } + scope.sortDirection = scope.sortReverse ? 'desc' : 'asc'; + }; + // sort function + scope.sortBy = function (item) { + if (scope.sortColumn === 'updateDate') { + return [ + -item['isFolder'], + item['updateDate'] + ]; + } else { + return [ + -item['isFolder'], + item['name'] + ]; + } + }; } var directive = { restrict: 'E', replace: true, - template: '
      {{item.name}}
      {{item.name}} {{item.name}} {{item.name}}
      ', + template: '
      {{item.name}}
      {{item.name}} {{item.name}} {{item.name}}
      {{item.name}}
      {{item.updateDate | date:\'medium\'}}
      ', scope: { items: '=', onDetailsHover: '=', @@ -13937,7 +15946,8 @@ Use this directive to generate a thumbnail grid of media items. onlyImages: '@', onlyFolders: '@', includeSubFolders: '@', - currentFolderId: '@' + currentFolderId: '@', + showMediaList: '=' }, link: link }; @@ -13989,9 +15999,15 @@ Use this directive to generate a thumbnail grid of media items. // start loading animation list view miniListView.loading = true; entityResource.getPagedChildren(miniListView.node.id, scope.entityType, miniListView.pagination).then(function (data) { + if (!data.items) { + data.items = []; + } + if (scope.onItemsLoaded) { + scope.onItemsLoaded({ items: data.items }); + } // update children miniListView.children = data.items; - _.each(miniListView.children, function (c) { + miniListView.children.forEach(function (c) { // child allowed by default c.allowed = true; // convert legacy icon for node @@ -14016,8 +16032,8 @@ Use this directive to generate a thumbnail grid of media items. }); // advanced item filtering is handled here if (scope.entityTypeFilter && scope.entityTypeFilter.filter && scope.entityTypeFilter.filterAdvanced) { - var filtered = angular.isFunction(scope.entityTypeFilter.filter) ? _.filter(miniListView.children, scope.entityTypeFilter.filter) : _.where(miniListView.children, scope.entityTypeFilter.filter); - _.each(filtered, function (node) { + var filtered = Utilities.isFunction(scope.entityTypeFilter.filter) ? _.filter(miniListView.children, scope.entityTypeFilter.filter) : _.where(miniListView.children, scope.entityTypeFilter.filter); + filtered.forEach(function (node) { return node.allowed = false; }); } @@ -14048,7 +16064,7 @@ Use this directive to generate a thumbnail grid of media items. scope.clickBreadcrumb = function (ancestor) { var found = false; scope.listViewAnimation = 'out'; - angular.forEach(miniListViewsHistory, function (historyItem, index) { + Utilities.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". @@ -14085,7 +16101,7 @@ Use this directive to generate a thumbnail grid of media items. }; function makeBreadcrumb() { scope.breadcrumb = []; - angular.forEach(miniListViewsHistory, function (historyItem) { + Utilities.forEach(miniListViewsHistory, function (historyItem) { scope.breadcrumb.push(historyItem.node); }); } @@ -14109,13 +16125,14 @@ Use this directive to generate a thumbnail grid of media items. var directive = { restrict: 'E', replace: true, - template: '

      {{ miniListView.node.name }}

       
      {{ child.name }}
      No items have been added Sorry, we can not find what you are looking for.
      ', + template: '

      {{ miniListView.node.name }}

       
      {{ child.name }}
      No items have been added Sorry, we can not find what you are looking for.
      ', scope: { node: '=', entityType: '@', startNodeId: '=', onSelect: '&', onClose: '&', + onItemsLoaded: '&', entityTypeFilter: '=' }, link: link @@ -14125,18 +16142,77 @@ Use this directive to generate a thumbnail grid of media items. angular.module('umbraco.directives').directive('umbMiniListView', MiniListViewDirective); }()); 'use strict'; + (function () { + 'use strict'; + angular.module('umbraco').component('umbMiniSearch', { + template: ' ', + controller: UmbMiniSearchController, + controllerAs: 'vm', + bindings: { + model: '=', + onStartTyping: '&?', + onSearch: '&?', + onBlur: '&?', + labelKey: '@?', + inputId: '@?' + } + }); + function UmbMiniSearchController($scope, localizationService) { + var vm = this; + vm.onKeyDown = onKeyDown; + vm.onChange = onChange; + vm.$onInit = onInit; + var searchDelay = _.debounce(function () { + $scope.$apply(function () { + if (vm.onSearch) { + vm.onSearch(); + } + }); + }, 500); + function onKeyDown(evt) { + //13: enter + switch (evt.keyCode) { + case 13: + if (vm.onSearch) { + vm.onSearch(); + } + break; + } + } + function onChange() { + if (vm.onStartTyping) { + vm.onStartTyping(); + } + searchDelay(); + } + function onInit() { + vm.inputId = vm.inputId || 'search_' + String.CreateGuid(); + setText(); + } + function setText() { + var keyToLocalize = vm.labelKey || 'general_search'; + localizationService.localize(keyToLocalize).then(function (data) { + // If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [ + if (data.indexOf('[') === -1) { + vm.text = data; + } + }); + } + } + }()); + 'use strict'; angular.module('umbraco.directives').directive('umbNestedContentEditor', [function () { var link = function link($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.model = Utilities.copy($scope.ngModel); $scope.nodeContext = $scope.model; // Find the selected tab var selectedTab = $scope.model.variants[0].tabs[0]; if ($scope.tabAlias) { - angular.forEach($scope.model.variants[0].tabs, function (tab) { + Utilities.forEach($scope.model.variants[0].tabs, function (tab) { if (tab.alias.toLowerCase() === $scope.tabAlias.toLowerCase()) { selectedTab = tab; return; @@ -14150,13 +16226,13 @@ Use this directive to generate a thumbnail grid of media items. // Tell inner controls we are submitting $scope.$broadcast('formSubmitting', { scope: $scope }); // Sync the values back - angular.forEach($scope.ngModel.variants[0].tabs, function (tab) { + Utilities.forEach($scope.ngModel.variants[0].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) { + Utilities.forEach(tab.properties, function (prop) { if (localPropsMap.hasOwnProperty(prop.alias)) { prop.value = localPropsMap[prop.alias].value; } @@ -14317,7 +16393,7 @@ Use this directive to generate a thumbnail grid of media items. var directive = { restrict: 'E', replace: true, - template: '
      {{ name }}
      {{ description }}
      Permissions: {{ permission.name }}
      Edit {{name}} Open {{name}} Remove {{name}}
      ', + template: '
      {{name}}
      {{description}}
      Permissions: {{permission.name}}
      Edit {{name}} Open {{name}} Remove {{name}}
      ', scope: { icon: '=?', name: '=', @@ -14361,6 +16437,7 @@ Use this directive to generate a pagination. total-pages="vm.pagination.totalPages" on-next="vm.nextPage" on-prev="vm.prevPage" + on-change="vm.changePage" on-go-to-page="vm.goToPage"> @@ -14379,10 +16456,11 @@ Use this directive to generate a pagination. vm.pagination = { pageNumber: 1, totalPages: 10 - } + }; vm.nextPage = nextPage; vm.prevPage = prevPage; + vm.changePage = changePage; vm.goToPage = goToPage; function nextPage(pageNumber) { @@ -14396,6 +16474,12 @@ Use this directive to generate a pagination. console.log(pageNumber); alert("prevpage"); } + + function changePage(pageNumber) { + // do magic here + console.log(pageNumber); + alert("changepage"); + } function goToPage(pageNumber) { // do magic here @@ -14426,6 +16510,11 @@ Use this directive to generate a pagination.
      • pageNumber: The page number
      +@param {callback=} onChange (binding): Callback method when changing page. +

      The callback returns:

      +
        +
      • pageNumber: The page number
      • +
      **/ (function () { 'use strict'; @@ -14518,9 +16607,7 @@ Use this directive to generate a pagination. scope.onGoToPage(scope.pageNumber); } if (scope.onChange) { - if (scope.onChange) { - scope.onChange({ 'pageNumber': scope.pageNumber }); - } + scope.onChange({ 'pageNumber': scope.pageNumber }); } }; var unbindPageNumberWatcher = scope.$watchCollection('[pageNumber, totalPages]', function (newValues, oldValues) { @@ -14534,7 +16621,7 @@ Use this directive to generate a pagination. var directive = { restrict: 'E', replace: true, - template: ' ', + template: ' ', scope: { pageNumber: '=', totalPages: '=', @@ -14552,38 +16639,6 @@ Use this directive to generate a pagination. 'use strict'; /** @ngdoc directive -@name umbraco.directives.directive:umbPasswordToggle -@restrict E -@scope - -@description -Added in Umbraco v. 7.7.4: Use this directive to render a password toggle. - -**/ - (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 link(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); - } - }; - return directive; - } - angular.module('umbraco.directives').directive('umbPasswordToggle', PasswordToggleDirective); - }()); - 'use strict'; - /** -@ngdoc directive @name umbraco.directives.directive:umbProgressBar @restrict E @scope @@ -14600,6 +16655,7 @@ Use this directive to generate a progress bar. @param {number} percentage (attribute): The progress in percentage. @param {string} size (attribute): The size (s, m). +@param {string} color (attribute): The color of the progress (primary, secondary, success, warning, danger). Success by default. **/ (function () { @@ -14608,10 +16664,11 @@ Use this directive to generate a progress bar. var directive = { restrict: 'E', replace: true, - template: ' ', + template: ' ', scope: { percentage: '@', - size: '@?' + size: '@?', + color: '@?' } }; return directive; @@ -14641,8 +16698,8 @@ Use this directive to render a circular progressbar.
      -@param {string} size (attribute): This parameter defines the width and the height of the circle in pixels. @param {string} percentage (attribute): Takes a number between 0 and 100 and applies it to the circle's highlight length. +@param {string} size (attribute): This parameter defines the width and the height of the circle in pixels. @param {string} color (attribute): The color of the highlight (primary, secondary, success, warning, danger). Success by default. **/ (function () { @@ -14677,9 +16734,9 @@ Use this directive to render a circular progressbar. replace: true, template: '
      {{ percentage }}%
      ', scope: { - size: '@?', percentage: '@', - color: '@' + size: '@?', + color: '@?' }, link: link }; @@ -14736,12 +16793,13 @@ For extra details about options and events take a look here: https://refreshless @param {object} ngModel (binding): Value for the slider. -@param {object} options (binding): Config object for the date picker. +@param {object} options (binding): Config object for the slider. @param {callback} onSetup (callback): onSetup gets triggered when the slider is initialized @param {callback} onUpdate (callback): onUpdate fires every time the slider values are changed. @param {callback} onSlide (callback): onSlide gets triggered when the handle is being dragged. @param {callback} onSet (callback): onSet will trigger every time a slider stops changing. @param {callback} onChange (callback): onChange fires when a user stops sliding, or when a slider value is changed by 'tap'. +@param {callback} onDrag (callback): onDrag fires when a connect element between handles is being dragged, while ignoring other updates to the slider values. @param {callback} onStart (callback): onStart fires when a handle is clicked (mousedown, or the equivalent touch events). @param {callback} onEnd (callback): onEnd fires when a handle is released (mouseup etc), or when a slide is canceled due to other reasons. **/ @@ -14758,6 +16816,7 @@ For extra details about options and events take a look here: https://refreshless onSlide: '&?', onSet: '&?', onChange: '&?', + onDrag: '&?', onStart: '&?', onEnd: '&?' } @@ -14793,6 +16852,7 @@ For extra details about options and events take a look here: https://refreshless var options = ctrl.options ? ctrl.options : defaultOptions; // create new slider noUiSlider.create(sliderInstance, options); + mergeTooltips(sliderInstance, 15, ' - '); if (ctrl.onSetup) { ctrl.onSetup({ slider: sliderInstance }); } @@ -14801,7 +16861,7 @@ For extra details about options and events take a look here: https://refreshless sliderInstance.noUiSlider.set(ctrl.ngModel); } // destroy the slider instance when the dom element is removed - angular.element(element).on('$destroy', function () { + $(element).on('$destroy', function () { sliderInstance.noUiSlider.off(); }); setUpCallbacks(); @@ -14866,6 +16926,20 @@ For extra details about options and events take a look here: https://refreshless }); }); } + // bind hook for drag + if (ctrl.onDrag) { + sliderInstance.noUiSlider.on('drag', function (values, handle, unencoded, tap, positions) { + $timeout(function () { + ctrl.onDrag({ + values: values, + handle: handle, + unencoded: unencoded, + tap: tap, + positions: positions + }); + }); + }); + } // bind hook for start if (ctrl.onStart) { sliderInstance.noUiSlider.on('start', function (values, handle, unencoded, tap, positions) { @@ -14896,6 +16970,77 @@ For extra details about options and events take a look here: https://refreshless } } } + // Merging overlapping tooltips: https://refreshless.com/nouislider/examples/#section-merging-tooltips + /** + * @param slider HtmlElement with an initialized slider + * @param threshold Minimum proximity (in percentages) to merge tooltips + * @param separator String joining tooltips + */ + function mergeTooltips(slider, threshold, separator) { + var textIsRtl = getComputedStyle(slider).direction === 'rtl'; + var isRtl = slider.noUiSlider.options.direction === 'rtl'; + var isVertical = slider.noUiSlider.options.orientation === 'vertical'; + var tooltips = slider.noUiSlider.getTooltips(); + var origins = slider.noUiSlider.getOrigins(); + // Move tooltips into the origin element. The default stylesheet handles this. + tooltips.forEach(function (tooltip, index) { + if (tooltip) { + origins[index].appendChild(tooltip); + } + }); + slider.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) { + var pools = [[]]; + var poolPositions = [[]]; + var poolValues = [[]]; + var atPool = 0; + // Assign the first tooltip to the first pool, if the tooltip is configured + if (tooltips[0]) { + pools[0][0] = 0; + poolPositions[0][0] = positions[0]; + poolValues[0][0] = values[0]; + } + for (var i = 1; i < positions.length; i++) { + if (!tooltips[i] || positions[i] - positions[i - 1] > threshold) { + atPool++; + pools[atPool] = []; + poolValues[atPool] = []; + poolPositions[atPool] = []; + } + if (tooltips[i]) { + pools[atPool].push(i); + poolValues[atPool].push(values[i]); + poolPositions[atPool].push(positions[i]); + } + } + pools.forEach(function (pool, poolIndex) { + var handlesInPool = pool.length; + for (var j = 0; j < handlesInPool; j++) { + var handleNumber = pool[j]; + if (j === handlesInPool - 1) { + var offset = 0; + poolPositions[poolIndex].forEach(function (value) { + offset += 1000 - 10 * value; + }); + var direction = isVertical ? 'bottom' : 'right'; + var last = isRtl ? 0 : handlesInPool - 1; + var lastOffset = 1000 - 10 * poolPositions[poolIndex][last]; + offset = (textIsRtl && !isVertical ? 100 : 0) + offset / handlesInPool - lastOffset; + // Filter to unique values + var tooltipValues = poolValues[poolIndex].filter(function (v, i, a) { + return a.indexOf(v) === i; + }); + // Center this tooltip over the affected handles + tooltips[handleNumber].innerHTML = tooltipValues.join(separator); + tooltips[handleNumber].style.display = 'block'; + tooltips[handleNumber].style[direction] = offset + '%'; + } else { + // Hide this tooltip + tooltips[handleNumber].style.display = 'none'; + } + } + }); + }); + } } angular.module('umbraco.directives').component('umbRangeSlider', umbRangeSlider); }()); @@ -14998,7 +17143,7 @@ Use this directive make an element sticky and follow the page when scrolling. `u
      -@param {string} icon (binding): The node icon. -@param {string} name (binding): The node name. -@param {string} published (binding): The node published state. +@param {array} items (binding): The items for the table. +@param {array} itemProperties (binding): The properties for the items to use in table. +@param {boolean} allowSelectAll (binding): Specify whether to allow select all. @param {function} onSelect (expression): Callback function when the row is selected. @param {function} onClick (expression): Callback function when the "Name" column link is clicked. @param {function} onSelectAll (expression): Callback function when selecting all items. @@ -15147,7 +17292,7 @@ Use this directive make an element sticky and follow the page when scrolling. `u }; } angular.module('umbraco.directives').component('umbTable', { - template: '
      Status
      {{item[column.alias]}}
      There are no items show in the list.
      ', + template: '
      Status
      {{item[column.alias]}}
      There are no items show in the list.
      ', controller: TableController, controllerAs: 'vm', bindings: { @@ -15164,6 +17309,52 @@ Use this directive make an element sticky and follow the page when scrolling. `u }); }()); 'use strict'; + (function () { + 'use strict'; + function umbTextarea($document) { + function autogrow(scope, element, attributes) { + if (!element.hasClass('autogrow')) { + // no autogrow for you today + return; + } + // get possible minimum height style + var minHeight = parseInt(window.getComputedStyle(element[0]).getPropertyValue('min-height')) || 0; + // prevent newlines in textbox + element.on('keydown', function (evt) { + if (evt.which === 13) { + } + }); + element.on('input', function (evt) { + element.css({ + height: 'auto', + minHeight: 0 + }); + var contentHeight = this.scrollHeight; + var borderHeight = 1; + var paddingHeight = 4; + element.css({ + minHeight: null, + // remove property + height: contentHeight + borderHeight + paddingHeight + 'px' // because we're using border-box + }); + }); + // watch model changes from the outside to adjust height + scope.$watch(attributes.ngModel, trigger); + // set initial size + trigger(); + function trigger() { + setTimeout(element.triggerHandler.bind(element, 'input'), 1); + } + } + var directive = { + restrict: 'E', + link: autogrow + }; + return directive; + } + angular.module('umbraco.directives').directive('textarea', umbTextarea); + }()); + 'use strict'; /** @ngdoc directive @name umbraco.directives.directive:umbTooltip @@ -15239,11 +17430,13 @@ Use this directive to render a tooltip. scope.tooltipStyles.left = 0; scope.tooltipStyles.top = 0; 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 overlay = $(event.target).closest('.umb-overlay'); + var container = overlay.length > 0 ? overlay : $('#contentwrapper'); + var rect = container[0].getBoundingClientRect(); + var containerLeft = rect.left; + var containerRight = containerLeft + rect.width; + var containerTop = rect.top; + var containerBottom = containerTop + rect.height; var elementHeight = null; var elementWidth = null; var position = { @@ -15257,26 +17450,31 @@ Use this directive to render a tooltip. elementWidth = el[0].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'; + if (overlay.length > 0) { + position.left = event.pageX - rect.left - elementWidth / 2; + position.top = event.pageY - rect.top; + } else { + // 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); @@ -15296,6 +17494,24 @@ Use this directive to render a tooltip. angular.module('umbraco.directives').directive('umbTooltip', TooltipDirective); }()); 'use strict'; + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); + } + function _nonIterableSpread() { + throw new TypeError('Invalid attempt to spread non-iterable instance'); + } + function _iterableToArray(iter) { + if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === '[object Arguments]') + return Array.from(iter); + } + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } + } /** * @ngdoc directive * @name umbraco.directives.directive:umbFileDropzone @@ -15314,11 +17530,11 @@ TODO } }) */ - angular.module('umbraco.directives').directive('umbFileDropzone', function ($timeout, Upload, localizationService, umbRequestHelper) { + angular.module('umbraco.directives').directive('umbFileDropzone', function ($timeout, Upload, localizationService, umbRequestHelper, overlayService, mediaHelper, mediaTypeHelper) { return { restrict: 'E', replace: true, - template: '

      Drag and drop your file(s) into the area

      • {{ file.name }}
      • {{ currentFile.name }}
      • {{ queued.name }}
      • {{ file.name }} "{{maxFileSize}}" {{file.serverErrorMessage}}
      ', + template: '

      Drag and drop your file(s) into the area

      • {{ file.name }}
      • {{currentFile.name}}
      • {{queued.name}}
      • {{file.name}} "{{maxFileSize}}" {{file.serverErrorMessage}}
      ', scope: { parentId: '@', contentTypeAlias: '@', @@ -15351,7 +17567,7 @@ TODO } function _filesQueued(files, event) { //Push into the queue - angular.forEach(files, function (file) { + Utilities.forEach(files, function (file) { if (_filterFile(file) === true) { if (file.$error) { scope.rejected.push(file); @@ -15371,20 +17587,11 @@ TODO }); 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; - } + // If we have Accepted Media Types, we will ask to choose Media Type, if Choose Media Type returns false, it only had one choice and therefor no reason to + if (scope.acceptedMediatypes && _requestChooseMediaTypeDialog() === false) { + scope.contentTypeAlias = 'umbracoAutoSelect'; _processQueueItem(); } - // More than one, open dialog - if (scope.acceptedMediatypes && scope.acceptedMediatypes.length > 1) { - _chooseMediaType(); - } } } function _processQueueItem() { @@ -15456,6 +17663,8 @@ TODO } } else if (evt.Message) { file.serverErrorMessage = evt.Message; + } else if (evt && typeof evt === 'string') { + file.serverErrorMessage = evt; } // If file not found, server will return a 404 and display this message if (status === 404) { @@ -15467,36 +17676,69 @@ TODO _processQueueItem(); }); } - function _chooseMediaType() { - scope.mediatypepickerOverlay = { - view: 'mediatypepicker', - title: 'Choose media type', - acceptedMediatypes: scope.acceptedMediatypes, - hideSubmitButton: true, - show: true, - submit: function submit(model) { - scope.contentTypeAlias = model.selectedType.alias; - scope.mediatypepickerOverlay.show = false; - scope.mediatypepickerOverlay = null; - _processQueueItem(); - }, - close: function close(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) { + function _requestChooseMediaTypeDialog() { + if (scope.queue.length === 0) { + // if queue has no items so there is nothing to choose a type for + return false; + } + if (scope.acceptedMediatypes.length === 1) { + // if only one accepted type, then we wont ask to choose. + return false; + } + var uploadFileExtensions = scope.queue.map(function (file) { + return mediaHelper.getFileExtension(file.name); + }); + var filteredMediaTypes = mediaTypeHelper.getTypeAcceptingFileExtensions(scope.acceptedMediatypes, uploadFileExtensions); + var mediaTypesNotFile = filteredMediaTypes.filter(function (mediaType) { + return mediaType.alias !== 'File'; + }); + if (mediaTypesNotFile.length <= 1) { + // if only one or less accepted types when we have filtered type 'file' out, then we wont ask to choose. + return false; + } + localizationService.localizeMany([ + 'defaultdialogs_selectMediaType', + 'mediaType_autoPickMediaType' + ]).then(function (translations) { + filteredMediaTypes.push({ + alias: 'umbracoAutoSelect', + name: translations[1], + icon: 'icon-wand' + }); + var dialog = { + view: 'itempicker', + filter: filteredMediaTypes.length > 8, + availableItems: filteredMediaTypes, + submit: function submit(model) { + scope.contentTypeAlias = model.selectedItem.alias; + _processQueueItem(); + overlayService.close(); + }, + close: function close() { + scope.queue.map(function (file) { + file.uploadStatus = 'error'; + file.serverErrorMessage = 'No files uploaded, no mediatype selected'; + scope.rejected.push(file); + }); + scope.queue = []; + overlayService.close(); + } + }; + dialog.title = translations[0]; + overlayService.open(dialog); + }); + return true; // yes, we did open the choose-media dialog, therefor we return true. + } + scope.handleFiles = function (files, event, invalidFiles) { + var allFiles = [].concat(_toConsumableArray(files), _toConsumableArray(invalidFiles)); + // add unique key for each files to use in ng-repeats + allFiles.forEach(function (file) { + file.key = String.CreateGuid(); + }); if (scope.filesQueued) { - scope.filesQueued(files, event); + scope.filesQueued(allFiles, event); } - _filesQueued(files, event); + _filesQueued(allFiles, event); }; } }; @@ -15524,6 +17766,11 @@ TODO //clear the element value - this allows us to pick the same file again and again el.val(''); }); + el.on('dragover dragenter', function () { + scope.$emit('isDragover', { value: true }); + }).on('dragleave dragend drop', function () { + scope.$emit('isDragover', { value: false }); + }); } }; } @@ -15552,6 +17799,7 @@ TODO fileManager.setFiles({ propertyAlias: vm.propertyAlias, culture: vm.culture, + segment: vm.segment, files: [] }); //clear the current files @@ -15599,6 +17847,7 @@ TODO /** Called when the component initializes */ function onInit() { $scope.$on('filesSelected', onFilesSelected); + $scope.$on('isDragover', isDragover); initialize(); } /** Called when the component has linked all elements, this is when the form controller is available */ @@ -15609,6 +17858,10 @@ TODO if (!vm.culture) { vm.culture = null; } + //normalize segment to null if it's not there + if (!vm.segment) { + vm.segment = null; + } // TODO: need to figure out what we can do for things like Nested Content var existingClientFiles = checkPendingClientFiles(); //create the property to show the list of files currently saved @@ -15641,9 +17894,13 @@ TODO if (!vm.culture) { vm.culture = null; } + //normalize segment to null if it's not there + if (!vm.segment) { + vm.segment = null; + } //check the file manager to see if there's already local files pending for this editor var existingClientFiles = _.map(_.filter(fileManager.getFiles(), function (f) { - return f.alias === vm.propertyAlias && f.culture === vm.culture; + return f.alias === vm.propertyAlias && f.culture === vm.culture && f.segment === vm.segment; }), function (f) { return f.file; }); @@ -15704,16 +17961,18 @@ TODO var index = i; //capture var isImage = mediaHelper.detectIfImageByExtension(files[i].name); - //save the file object to the files collection - vm.files.push({ + var extension = getExtension(files[i].name); + var f = { isImage: isImage, - extension: getExtension(files[i].name), + extension: extension, fileName: files[i].name, isClientSide: true - }); + }; + // Save the file object to the files collection + vm.files.push(f); //special check for a comma in the name newVal += files[i].name.split(',').join('-') + ','; - if (isImage) { + if (isImage || extension === 'svg') { var deferred = $q.defer(); reader.onload = function (e) { vm.files[index].fileSrc = e.target.result; @@ -15741,7 +18000,8 @@ TODO fileManager.setFiles({ propertyAlias: vm.propertyAlias, files: args.files, - culture: vm.culture + culture: vm.culture, + segment: vm.segment }); updateModelFromSelectedFiles(args.files).then(function (newVal) { angularHelper.safeApply($scope, function () { @@ -15754,15 +18014,21 @@ TODO angularHelper.safeApply($scope); } } + function isDragover(e, args) { + vm.dragover = args.value; + angularHelper.safeApply($scope); + } } ; var umbPropertyFileUploadComponent = { - template: '

      Click to upload

      {{file.fileName}}
      {{file.fileName}}
      ', + template: '

      Click to upload

      {{file.fileName}}
      {{file.fileName}}
      ', bindings: { culture: '@?', + segment: '@?', propertyAlias: '@', value: '<', hideSelection: '<', + dragover: '<', /** * Called when a file is selected on this instance */ @@ -15772,7 +18038,8 @@ TODO */ onFilesChanged: '&', onInit: '&', - required: '=' + required: '=', + acceptFileExt: ''; return { restrict: 'E', - scope: { rebuild: '=' }, + scope: { + rebuild: '=', + acceptFileExt: '
      ', - link: function link(scope, el, attrs) { + template: '
      ' + innerTemplate + '
      ', + link: function link(scope, el) { scope.$watch('rebuild', function (newVal, oldVal) { if (newVal && newVal !== oldVal) { //recompile it! - el.html(''); + el.html(innerTemplate); $compile(el.contents())(scope); } }); @@ -15820,6 +18092,7 @@ TODO vm.cancelChange = cancelChange; vm.showOldPass = showOldPass; vm.showCancelBtn = showCancelBtn; + vm.newPasswordKeyUp = newPasswordKeyUp; var unsubscribe = []; function resetModel(isNew) { //the model config will contain an object, if it does not we'll create defaults @@ -15857,8 +18130,12 @@ TODO if (vm.config.minPasswordLength === undefined) { vm.config.minPasswordLength = 0; } + // Check non-alpha pwd settings for tooltip display + if (vm.config.minNonAlphaNumericChars === undefined) { + vm.config.minNonAlphaNumericChars = 0; + } //set the model defaults - if (!angular.isObject(vm.passwordValues)) { + if (!Utilities.isObject(vm.passwordValues)) { //if it's not an object then just create a new one vm.passwordValues = { newPassword: null, @@ -15876,6 +18153,8 @@ TODO vm.passwordValues.reset = null; vm.passwordValues.answer = null; } + // set initial value for new password value + vm.passwordVal = vm.passwordValues.newPassword; //the value to compare to match passwords if (!isNew) { vm.passwordValues.confirm = ''; @@ -15939,9 +18218,12 @@ TODO return vm.config.disableToggle !== true && vm.config.hasPassword; } ; + function newPasswordKeyUp(event) { + vm.passwordVal = event.target.value; + } } var component = { - template: '
      Password has been reset to:
      {{vm.passwordValues.generatedPassword}}
      {{changePasswordForm.resetPassword.errorMsg}} Required {{changePasswordForm.oldPassword.errorMsg}} Required Minimum {{vm.config.minPasswordLength}} characters {{changePasswordForm.password.errorMsg}} The confirmed password doesn\'t match the new password!
      ', + template: '
      Password has been reset to:
      {{vm.passwordValues.generatedPassword}}
      {{changePasswordForm.resetPassword.errorMsg}} Required {{changePasswordForm.oldPassword.errorMsg}} Required Minimum {{vm.config.minPasswordLength}} characters {{changePasswordForm.password.errorMsg}} The confirmed password doesn\'t match the new password!
      ', controller: ChangePasswordController, controllerAs: 'vm', bindings: { @@ -16009,7 +18291,7 @@ Use this directive to render a user group preview, where you can see the permiss var directive = { restrict: 'E', replace: true, - template: '
      {{ name }}
      Sections: {{ section.name }} All sections
      Content start node: No start node selected {{ contentStartNode.name }}
      Media start node: No start node selected {{ mediaStartNode.name }}
      Permissions: {{ permission.name }}
      ', + template: '
      {{ name }}
      Sections: {{ section.name }} All sections
      Content start node: No start node selected {{ contentStartNode.name }}
      Media start node: No start node selected {{ mediaStartNode.name }}
      Permissions: {{ permission.name }}
      ', scope: { icon: '=?', name: '=', @@ -16039,7 +18321,7 @@ Use this directive to render a user group preview, where you can see the permiss var directive = { restrict: 'E', replace: true, - template: '
      {{ name }}
      ', + template: '
      {{ name }}
      ', scope: { avatars: '=?', name: '=', @@ -16073,7 +18355,7 @@ Use this directive to render a user group preview, where you can see the permiss return function (scope, el, attrs) { var totalOffset = 0; var offsety = parseInt(attrs.autoScale, 10); - var window = angular.element($window); + var window = $($window); if (offsety !== undefined) { totalOffset += offsety; } @@ -16113,8 +18395,8 @@ Use this directive to render a user group preview, where you can see the permiss //Check if any child items in mutation.target contain an input var childInputs = tabbableService.tabbable(mutation.target); //For each item in childInputs - override or set HTML attribute tabindex="-1" - angular.forEach(childInputs, function (element) { - angular.element(element).attr('tabindex', '-1'); + Utilities.forEach(childInputs, function (element) { + $(element).attr('tabindex', '-1'); }); } } @@ -16193,6 +18475,18 @@ Use this directive to render a user group preview, where you can see the permiss }; }); 'use strict'; + angular.module('umbraco.directives').directive('umbDroppable', function ($timeout) { + return { + restrict: 'A', + link: function link(scope, element, attrs) { + $timeout(function () { + var options = scope.$eval(attrs.umbDroppable); + element.droppable(options); + }); + } + }; + }); + 'use strict'; angular.module('umbraco.directives').directive('umbIsolateForm', function () { return { restrict: 'A', @@ -16218,12 +18512,12 @@ Use this directive to render a user group preview, where you can see the permiss
           
      @@ -16240,7 +18534,7 @@ Use this directive to render a user group preview, where you can see the permiss function ($document, $timeout) { return { restrict: 'A', - link: function link(scope, element, attr) { + link: function link(scope, element) { var listItems = []; var currentIndex = 0; var focusSet = false; @@ -16265,7 +18559,7 @@ Use this directive to render a user group preview, where you can see the permiss function checkFocus() { var found = false; // check if any element has focus - angular.forEach(listItems, function (item, index) { + Utilities.forEach(listItems, function (item, index) { if ($(item).is(':focus')) { // if an element already has focus set the // currentIndex so we navigate from that element @@ -16283,7 +18577,7 @@ Use this directive to render a user group preview, where you can see the permiss } function arrowDown() { if (currentIndex < listItems.length - 1) { - // only bump the current index if the focus is already + // only bump the current index if the focus is already // set else we just want to focus the first element if (focusSet) { currentIndex++; @@ -16311,6 +18605,50 @@ Use this directive to render a user group preview, where you can see the permiss } ]); 'use strict'; + angular.module('umbraco.directives').directive('umbOverflowChecker', function ($parse, $timeout, windowResizeListener) { + return { + restrict: 'A', + link: function link(scope, element, attrs) { + var overflow = $parse(attrs.onOverflow); + var scrollElement = element[0]; + var container = element[0].parentElement; + function checkOverflow() { + $timeout(function () { + var scrollElementScrollWidth = scrollElement.scrollWidth; + var containerScrollWidth = container.scrollWidth; + var overflowLeft = scrollElement.scrollLeft; + var overflowRight = containerScrollWidth - scrollElementScrollWidth + overflowLeft; + scope.$evalAsync(function () { + return overflow(scope, { + overflowLeft: overflowLeft, + overflowRight: overflowRight + }); + }); + }, 50); + } + function scrollTo(event, options) { + $timeout(function () { + if (options.position === 'end') { + scrollElement.scrollLeft = scrollElement.scrollWidth - scrollElement.clientWidth; + } + if (options.position === 'start') { + scrollElement.scrollLeft = 0; + } + }, 50); + } + scrollElement.addEventListener('scroll', checkOverflow); + windowResizeListener.register(checkOverflow); + scope.$on('$destroy', function () { + scrollElement.removeEventListener('scroll', checkOverflow); + windowResizeListener.unregister(checkOverflow); + }); + scope.$on('umbOverflowChecker.checkOverflow', checkOverflow); + scope.$on('umbOverflowChecker.scrollTo', scrollTo); + checkOverflow(); + } + }; + }); + 'use strict'; /** * @ngdoc directive * @name umbraco.directives.directive:noDirtyCheck @@ -16322,6 +18660,10 @@ Use this directive to render a user group preview, where you can see the permiss restrict: 'A', require: 'ngModel', link: function link(scope, elm, attrs, ctrl) { + // if "no-dirty-check" attribute is explicitly falsy, then skip and use default behaviour. In all other cases we consider it truthy + var skipNoDirtyCheck = attrs.noDirtyCheck === '0' || attrs.noDirtyCheck === 0 || attrs.noDirtyCheck.toString().toLowerCase() === 'false'; + if (skipNoDirtyCheck) + return; var alwaysFalse = { get: function get() { return false; @@ -16348,20 +18690,21 @@ Use this directive to render a user group preview, where you can see the permiss scope: { form: '=?' }, link: function link(scope, element, attr, ctrl) { var formMgr = ctrl.length > 1 ? ctrl[1] : null; + var hiddenClass = 'ng-hide'; //We can either get the form submitted status by the parent directive valFormManager //or we can check upwards in the DOM for the css class... lets try both :) //The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot //reset the status. var submitted = element.closest('.show-validation').length > 0 || formMgr && formMgr.showValidation; if (!submitted) { - element.hide(); + element[0].classList.add(hiddenClass); } var unsubscribe = []; unsubscribe.push(scope.$on('formSubmitting', function (ev, args) { - element.show(); + element[0].classList.remove(hiddenClass); })); unsubscribe.push(scope.$on('formSubmitted', function (ev, args) { - element.hide(); + element[0].classList.add(hiddenClass); })); //no isolate scope to listen to element destroy element.on('$destroy', function () { @@ -16491,10 +18834,69 @@ Use this directive to render a user group preview, where you can see the permiss * 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, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService) { + function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) { var SHOW_VALIDATION_CLASS_NAME = 'show-validation'; + var SHOW_VALIDATION_Type_CLASS_NAME = 'show-validation-type-'; var SAVING_EVENT_NAME = 'formSubmitting'; var SAVED_EVENT_NAME = 'formSubmitted'; + function notify(scope) { + scope.$broadcast('valStatusChanged', { form: scope.formCtrl }); + } + function ValFormManagerController($scope) { + //This exposes an API for direct use with this directive + // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and + // because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is + // an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager + // in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing) + $scope.valFormManager = this; + 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 + ]); + })); + }; + this.isShowingValidation = function () { + return $scope.showValidation === true; + }; + this.getValidationMessageType = function () { + return $scope.valMsgType; + }; + this.notify = notify; + this.isValid = function () { + return !$scope.formCtrl.$invalid; + }; + //Ensure to remove the event handlers when this instance is destroyted + $scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + } + /** + * Find's the valFormManager in the scope/DOM hierarchy + * @param {any} scope + * @param {any} ctrls + * @param {any} index + */ + function getAncestorValFormManager(scope, ctrls, index) { + // first check the normal directive inheritance which relies on DOM inheritance + var found = ctrls[index]; + if (found) { + return found; + } + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain(scope, function (s) { + return s && s.valFormManager && s.valFormManager.constructor.name === 'ValFormManagerController'; + }); + return found ? found.valFormManager : null; + } return { require: [ 'form', @@ -16502,28 +18904,7 @@ Use this directive to render a user group preview, where you can see the permiss '^^?valSubView' ], restrict: 'A', - controller: function controller($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 - ]); - })); - }; - this.showValidation = $scope.showValidation === true; - //Ensure to remove the event handlers when this instance is destroyted - $scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - }, + controller: ValFormManagerController, link: function link(scope, element, attr, ctrls) { function notifySubView() { if (subView) { @@ -16533,10 +18914,12 @@ Use this directive to render a user group preview, where you can see the permiss }); } } - var formCtrl = ctrls[0]; - var parentFormMgr = ctrls.length > 0 ? ctrls[1] : null; + var formCtrl = scope.formCtrl = ctrls[0]; + var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1); var subView = ctrls.length > 1 ? ctrls[2] : null; var labels = {}; + var valMsgType = 2; + // error var labelKeys = [ 'prompt_unsavedChanges', 'prompt_unsavedChangesWarning', @@ -16549,33 +18932,48 @@ Use this directive to render a user group preview, where you can see the permiss labels.discardChangesButton = values[2]; labels.stayButton = values[3]; }); - //watch the list of validation errors to notify the application of any validation changes + var lastValidationMessageType = null; + function setValidationMessageType(type) { + removeValidationMessageType(); + scope.valMsgType = type; + // overall a copy of message types from notifications.service: + var postfix = ''; + switch (type) { + case 0: + //save + break; + case 1: + //info + postfix = 'info'; + break; + case 2: + //error + postfix = 'error'; + break; + case 3: + //success + postfix = 'success'; + break; + case 4: + //warning + postfix = 'warning'; + break; + } + var cssClass = SHOW_VALIDATION_Type_CLASS_NAME + postfix; + element.addClass(cssClass); + lastValidationMessageType = cssClass; + } + function removeValidationMessageType() { + if (lastValidationMessageType) { + element.removeClass(lastValidationMessageType); + lastValidationMessageType = null; + } + } + // watch the list of validation errors to notify the application of any validation changes scope.$watch(function () { - //the validators are in the $error collection: https://docs.angularjs.org/api/ng/type/form.FormController#$error - //since each key is the validator name (i.e. 'required') we can't just watch the number of keys, we need to watch - //the sum of the items inside of each key - //get the lengths of each array for each key in the $error collection - var validatorLengths = _.map(formCtrl.$error, function (val, key) { - // if there are child ng-forms, include the $error collections in those as well - var innerErrorCount = _.reduce(_.map(val, function (v) { - return _.reduce(_.map(v.$error, function (e) { - return e.length; - }), function (m, n) { - return m + n; - }); - }), function (memo, num) { - return memo + num; - }); - return val.length + innerErrorCount; - }); - //sum up all numbers in the resulting array - var sum = _.reduce(validatorLengths, function (memo, num) { - return memo + num; - }, 0); - //this is the value we watch to notify of any validation changes on the form - return sum; + return formCtrl.$invalid; }, function (e) { - scope.$broadcast('valStatusChanged', { form: formCtrl }); + notify(scope); notifySubView(); //find all invalid elements' .control-group's and apply the error class var inError = element.find('.control-group .ng-invalid').closest('.control-group'); @@ -16590,16 +18988,27 @@ Use this directive to render a user group preview, where you can see the permiss // 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 || parentFormMgr && parentFormMgr.showValidation) { + if (serverValidationManager.items.length > 0 || parentFormMgr && parentFormMgr.isShowingValidation()) { element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; + var parentValMsgType = parentFormMgr ? parentFormMgr.getValidationMessageType() : 2; + setValidationMessageType(parentValMsgType || 2); notifySubView(); } var unsubscribe = []; //listen for the forms saving event unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) { + var messageType = 2; + //error + switch (args.action) { + case 'save': + messageType = 4; + //warning + break; + } element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; + setValidationMessageType(messageType); notifySubView(); //set the flag so we can check to see if we should display the error. isSavingNewItem = $routeParams.create; @@ -16608,11 +19017,9 @@ Use this directive to render a user group preview, where you can see the permiss unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) { //remove validation class element.removeClass(SHOW_VALIDATION_CLASS_NAME); + removeValidationMessageType(); scope.showValidation = false; notifySubView(); - //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(); })); var confirmed = false; //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but @@ -16649,7 +19056,7 @@ Use this directive to render a user group preview, where you can see the permiss var parts = nextPath.split('?'); var query = {}; if (parts.length > 1) { - _.each(parts[1].split('&'), function (q) { + parts[1].split('&').forEach(function (q) { var keyVal = q.split('='); query[keyVal[0]] = keyVal[1]; }); @@ -16674,6 +19081,8 @@ Use this directive to render a user group preview, where you can see the permiss unsubscribe[u](); } }); + // TODO: I'm unsure why this exists, i believe this may be a hack for something like tinymce which might automatically + // change a form value on load but we need it to be $pristine? $timeout(function () { formCtrl.$setPristine(); }, 1000); @@ -16749,14 +19158,17 @@ Use this directive to render a user group preview, where you can see the permiss * 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 valFormManager directive must be placed on the containing form. +* We don't set the validity of this validator to false when client side validation fails, only when server side +* validation fails however we do respond to the client side validation changes to display error and adjust UI state. **/ - function valPropertyMsg(serverValidationManager, localizationService) { + function valPropertyMsg(serverValidationManager, localizationService, angularHelper) { return { require: [ '^^form', '^^valFormManager', '^^umbProperty', - '?^^umbVariantContent' + '?^^umbVariantContent', + '?^^valPropertyMsg' ], replace: true, restrict: 'E', @@ -16766,6 +19178,7 @@ Use this directive to render a user group preview, where you can see the permiss var unsubscribe = []; var watcher = null; var hasError = false; + // tracks if there is a child error or an explicit error //create properties on our custom scope so we can use it in our template scope.errorMsg = ''; //the property form controller api @@ -16779,15 +19192,16 @@ Use this directive to render a user group preview, where you can see the permiss var currentProperty = umbPropCtrl.property; scope.currentProperty = currentProperty; var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; + // validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type) + var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; var labels = {}; - localizationService.localize('errors_propertyHasErrors').then(function (data) { - labels.propertyHasErrors = data; - }); + var showValidation = false; if (umbVariantCtrl) { //if we are inside of an umbVariantContent directive var currentVariant = umbVariantCtrl.editor.content; // Lets check if we have variants and we are on the default language then ... - if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) { + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { //This property is locked cause its a invariant property shown on a non-default language. //Therefor do not validate this field. return; @@ -16800,7 +19214,7 @@ Use this directive to render a user group preview, where you can see the permiss //this can be null if no property was assigned if (scope.currentProperty) { //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, ''); + var err = serverValidationManager.getPropertyError(umbPropCtrl.getValidationPath(), null, '', null); //if there's an error message use it if (err && err.errorMsg) { return err.errorMsg; @@ -16810,36 +19224,91 @@ Use this directive to render a user group preview, where you can see the permiss } return labels.propertyHasErrors; } - // 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. + // check the current errors in the form (and recursive sub forms), if there is 1 or 2 errors + // we can check if those are valPropertyMsg or valServer and can clear our error in those cases. + function checkAndClearError() { + var errCount = angularHelper.countAllFormErrors(formCtrl); + if (errCount === 0) { + return true; + } + if (errCount > 2) { + return false; + } + var hasValServer = Utilities.isArray(formCtrl.$error.valServer); + if (errCount === 1 && hasValServer) { + return true; + } + var hasOwnErr = hasExplicitError(); + if (errCount === 1 && hasOwnErr || errCount === 2 && hasOwnErr && hasValServer) { + var propertyValidationPath = umbPropCtrl.getValidationPath(); + // check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves + if (isLastServerError(propertyValidationPath)) { + serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, '', currentSegment); + return true; + } + return false; + } + return false; + } + // returns true if there is an explicit valPropertyMsg validation error on the form + function hasExplicitError() { + return Utilities.isArray(formCtrl.$error.valPropertyMsg); + } + // returns true if there is only a single server validation error for this property validation key in it's validation path + function isLastServerError(propertyValidationPath) { + var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath(propertyValidationPath, currentCulture, currentSegment, { matchType: 'prefix' }); + if (nestedErrs.length === 0 || nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationPath) { + return true; + } + return false; + } + // a custom $validator function called on when each child ngModelController changes a value. + function resetServerValidityValidator(fieldController) { + var storedFieldController = fieldController; + // pin a reference to this + return function (modelValue, viewValue) { + // if the ngModelController value has changed, then we can check and clear the error + if (storedFieldController.$dirty) { + if (checkAndClearError()) { + resetError(); + } + } + return true; // this validator is always 'valid' + }; + } + // collect all ng-model controllers recursively within the umbProperty form + // until it reaches the next nested umbProperty form + function collectAllNgModelControllersRecursively(controls, ngModels) { + controls.forEach(function (ctrl) { + if (angularHelper.isForm(ctrl)) { + // if it's not another umbProperty form then recurse + if (ctrl.$name !== formCtrl.$name) { + collectAllNgModelControllersRecursively(ctrl.$getControls(), ngModels); + } + } else if (ctrl.hasOwnProperty('$modelValue') && Utilities.isObject(ctrl.$validators)) { + ngModels.push(ctrl); + } + }); + } + // We start the watch when there's server validation errors detected. + // We watch on the current form's properties and on first watch or if they are dynamically changed + // we find all ngModel controls recursively on this form (but stop recursing before we get to the next) + // umbProperty form). Then for each ngModelController we assign a new $validator. This $validator + // will execute whenever the value is changed which allows us to check and reset the server validator function startWatch() { - //if there's not already a watch if (!watcher) { - watcher = scope.$watch('currentProperty.value', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) { - return; - } - var errCount = 0; - for (var e in formCtrl.$error) { - if (angular.isArray(formCtrl.$error[e])) { - errCount++; + watcher = scope.$watchCollection(function () { + return formCtrl; + }, function (updatedFormController) { + var childControls = updatedFormController.$getControls(); + var ngModels = []; + collectAllNgModelControllersRecursively(childControls, ngModels); + ngModels.forEach(function (x) { + if (!x.$validators.serverValidityResetter) { + x.$validators.serverValidityResetter = resetServerValidityValidator(x); } - } - //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 === 0 || errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg) || formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer)) { - scope.errorMsg = ''; - formCtrl.$setValidity('valPropertyMsg', true); - } else if (showValidation && scope.errorMsg === '') { - formCtrl.$setValidity('valPropertyMsg', false); - scope.errorMsg = getErrorMsg(); - } - }, true); + }); + }); } } //clear the watch when the property validator is valid again @@ -16849,94 +19318,147 @@ Use this directive to render a user group preview, where you can see the permiss watcher = null; } } + function resetError() { + stopWatch(); + hasError = false; + formCtrl.$setValidity('valPropertyMsg', true); + scope.errorMsg = ''; + } + // This deals with client side validation changes and is executed anytime validators change on the containing + // valFormManager. This allows us to know when to display or clear the property error data for non-server side errors. function checkValidationStatus() { if (formCtrl.$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 + //hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe return; } //if there are any errors in the current property form that are not valPropertyMsg else if (_.without(_.keys(formCtrl.$error), 'valPropertyMsg').length > 0) { + // errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared + if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) { + resetError(); + // if there's no value, the controls can be reset, which clears the error state on formCtrl + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + try { + for (var _iterator = formCtrl.$getControls()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var control = _step.value; + control.$setValidity(); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return != null) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + return; + } 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 = ''; + resetError(); } } else { - hasError = false; - scope.errorMsg = ''; + resetError(); } } - //if there's any remaining errors in the server validation service then we should show them. - var showValidation = serverValidationManager.items.length > 0; - if (!showValidation) { - //We can either get the form submitted status by the parent directive valFormManager (if we add a property to it) - //or we can just check upwards in the DOM for the css class (easier for now). - //The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot - //reset the status. - showValidation = element.closest('.show-validation').length > 0; - } - //listen for form validation changes. - //The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then - // subscribing to this single watch. - valFormManager.onValidationStatusChanged(function (evt, args) { - checkValidationStatus(); - }); - //listen for the forms saving event - unsubscribe.push(scope.$on('formSubmitting', function (ev, args) { - showValidation = true; - if (hasError && scope.errorMsg === '') { - scope.errorMsg = getErrorMsg(); - startWatch(); - } 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.currentProperty) { - //this can be null if no property was assigned - unsubscribe.push(serverValidationManager.subscribe(scope.currentProperty.alias, currentCulture, '', 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(); + function onInit() { + localizationService.localize('errors_propertyHasErrors').then(function (data) { + labels.propertyHasErrors = data; + //if there's any remaining errors in the server validation service then we should show them. + showValidation = serverValidationManager.items.length > 0; + if (!showValidation) { + //We can either get the form submitted status by the parent directive valFormManager (if we add a property to it) + //or we can just check upwards in the DOM for the css class (easier for now). + //The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot + //reset the status. + showValidation = element.closest('.show-validation').length > 0; } - })); + //listen for form validation changes. + //The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then + // subscribing to this single watch. + // TODO: Really? Since valFormManager is watching a countof all errors which is more overhead than watching formCtrl.$invalid + // and there's a TODO there that it should just watch formCtrl.$invalid + valFormManager.onValidationStatusChanged(function (evt, args) { + checkValidationStatus(); + }); + //listen for the forms saving event + unsubscribe.push(scope.$on('formSubmitting', function (ev, args) { + showValidation = true; + if (hasError && scope.errorMsg === '') { + scope.errorMsg = getErrorMsg(); + startWatch(); + } else if (!hasError) { + resetError(); + } + })); + //listen for the forms saved event + unsubscribe.push(scope.$on('formSubmitted', function (ev, args) { + showValidation = false; + resetError(); + })); + if (scope.currentProperty) { + //this can be null if no property was assigned + // listen for server validation changes for property validation path prefix. + // 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. + var serverValidationManagerCallback = function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { + var hadError = hasError; + hasError = !isValid; + if (hasError) { + //set the error message to the server message + scope.errorMsg = propertyErrors.length > 1 ? labels.propertyHasErrors : propertyErrors[0].errorMsg || labels.propertyHasErrors; + //flag that the current validator is invalid + formCtrl.$setValidity('valPropertyMsg', false); + startWatch(); + // This check is required in order to be able to reset ourselves and is typically for complex editor + // scenarios where the umb-property itself doesn't contain any ng-model controls which means that the + // above serverValidityResetter technique will not work to clear valPropertyMsg errors. + // In order for this to work we rely on the current form controller's $pristine state. This means that anytime + // the form is submitted whether there are validation errors or not the state must be reset... this is automatically + // taken care of with the formHelper.resetForm method that should be used in all cases. $pristine is required because it's + // a value that is cascaded to all form controls based on the hierarchy of child ng-model controls. This allows us to easily + // know if a value has changed. The alternative is what we used to do which was to put a deep $watch on the entire complex value + // which is hugely inefficient. + if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) { + var propertyValidationPath = umbPropCtrl.getValidationPath(); + serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, '', currentSegment); + resetError(); + } + } else { + resetError(); + } + }; + unsubscribe.push(serverValidationManager.subscribe(umbPropCtrl.getValidationPath(), currentCulture, '', serverValidationManagerCallback, currentSegment, { matchType: 'prefix' } // match property validation path prefix +)); + } + }); } //when the scope is disposed we need to unsubscribe scope.$on('$destroy', function () { stopWatch(); - for (var u in unsubscribe) { - unsubscribe[u](); - } + unsubscribe.forEach(function (u) { + return u(); + }); }); + onInit(); } }; } @@ -16968,8 +19490,8 @@ Use this directive to render a user group preview, where you can see the permiss link: function link(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)) { + // Check whether the scope has a valPropertyValidator method + if (!scope.valPropertyValidator || !Utilities.isFunction(scope.valPropertyValidator)) { throw new Error('val-property-validator directive must specify a function to call'); } // Validation method @@ -17116,20 +19638,21 @@ Use this directive to render a user group preview, where you can see the permiss scope: {}, link: function link(scope, element, attr, ctrls) { var modelCtrl = ctrls[0]; - var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null; + var umbPropCtrl = ctrls[1]; if (!umbPropCtrl) { //we cannot proceed, this validator will be disabled return; } // optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages. - var umbVariantCtrl = ctrls.length > 2 ? ctrls[2] : null; + var umbVariantCtrl = ctrls[2]; var currentProperty = umbPropCtrl.property; var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; if (umbVariantCtrl) { //if we are inside of an umbVariantContent directive var currentVariant = umbVariantCtrl.editor.content; // Lets check if we have variants and we are on the default language then ... - if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) { + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { //This property is locked cause its a invariant property shown on a non-default language. //Therefor do not validate this field. return; @@ -17148,8 +19671,10 @@ Use this directive to render a user group preview, where you can see the permiss fieldName = attr.valServer; } } - //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 + // Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works + var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : currentProperty.alias; + // 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 @@ -17161,13 +19686,13 @@ Use this directive to render a user group preview, where you can see the permiss watcher = scope.$watch(function () { return modelCtrl.$modelValue; }, function (newValue, oldValue) { - if (!newValue || angular.equals(newValue, oldValue)) { + if (!newValue || Utilities.equals(newValue, oldValue)) { return; } if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); //clear the server validation entry - serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName); + serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, fieldName, currentSegment); stopWatch(); } }, true); @@ -17180,7 +19705,7 @@ Use this directive to render a user group preview, where you can see the permiss } } //subscribe to the server validation changes - unsubscribe.push(serverValidationManager.subscribe(currentProperty.alias, currentCulture, fieldName, function (isValid, propertyErrors, allErrors) { + function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { if (!isValid) { modelCtrl.$setValidity('valServer', false); //assign an error msg property to the current validator @@ -17192,12 +19717,13 @@ Use this directive to render a user group preview, where you can see the permiss modelCtrl.errorMsg = ''; stopWatch(); } - })); + } + unsubscribe.push(serverValidationManager.subscribe(propertyValidationPath, currentCulture, fieldName, serverValidationManagerCallback, currentSegment)); scope.$on('$destroy', function () { stopWatch(); - for (var u in unsubscribe) { - unsubscribe[u](); - } + unsubscribe.forEach(function (u) { + return u(); + }); }); } }; @@ -17261,33 +19787,147 @@ Use this directive to render a user group preview, where you can see the permiss angular.module('umbraco.directives.validation').directive('valServerField', valServerField); 'use strict'; /** -* @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. + * @ngdoc directive + * @name umbraco.directives.directive:valServerMatch + * @restrict A + * @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data + * contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against + * a property validation key. The attribute value can be in multiple value types: + * - STRING = The property validation key to have an exact match on. If matched, then the form will have a valServerMatch validator applied. + * - OBJECT = A dictionary where the key is the match type: "contains", "prefix", "suffix" and the value is either: + * - ARRAY = A list of property validation keys to match on. If any are matched then the form will have a valServerMatch validator applied. + * - OBJECT = A dictionary where the key is the validator error name applied to the form and the value is the STRING of the property validation key to match on +**/ + function valServerMatch(serverValidationManager) { + return { + require: [ + 'form', + '^^umbProperty', + '?^^umbVariantContent' + ], + restrict: 'A', + scope: { valServerMatch: '=' }, + link: function link(scope, element, attr, ctrls) { + var formCtrl = ctrls[0]; + var umbPropCtrl = ctrls[1]; + if (!umbPropCtrl) { + //we cannot proceed, this validator will be disabled + return; + } + // optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages. + var umbVariantCtrl = ctrls[2]; + var currentProperty = umbPropCtrl.property; + var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; + if (umbVariantCtrl) { + //if we are inside of an umbVariantContent directive + var currentVariant = umbVariantCtrl.editor.content; + // Lets check if we have variants and we are on the default language then ... + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { + //This property is locked cause its a invariant property shown on a non-default language. + //Therefor do not validate this field. + return; + } + } + // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language. + currentCulture = currentCulture || 'invariant'; + var unsubscribe = []; + function bindCallback(validationKey, matchVal, matchType) { + if (!matchVal) + return; + if (Utilities.isString(matchVal)) { + matchVal = [matchVal]; // normalize to an array since the value can also natively be an array + } + // match for each string in the array + matchVal.forEach(function (m) { + unsubscribe.push(serverValidationManager.subscribe(m, currentCulture, '', // the callback + function (isValid, propertyErrors, allErrors) { + if (!isValid) { + formCtrl.$setValidity(validationKey, false); + } else { + formCtrl.$setValidity(validationKey, true); + } + }, currentSegment, matchType ? { matchType: matchType } : null // specify the match type +)); + }); + } + if (Utilities.isObject(scope.valServerMatch)) { + var allowedKeys = [ + 'contains', + 'prefix', + 'suffix' + ]; + Object.keys(scope.valServerMatch).forEach(function (matchType) { + if (allowedKeys.indexOf(matchType) === -1) { + throw 'valServerMatch dictionary keys must be one of ' + allowedKeys.join(); + } + var matchVal = scope.valServerMatch[matchType]; + if (Utilities.isObject(matchVal)) { + // as an object, the key will be the validation error instead of the default "valServerMatch" + Object.keys(matchVal).forEach(function (valKey) { + // matchVal[valKey] can be an ARRAY or a STRING + bindCallback(valKey, matchVal[valKey], matchType); + }); + } else { + // matchVal can be an ARRAY or a STRING + bindCallback('valServerMatch', matchVal, matchType); + } + }); + } else if (Utilities.isString(scope.valServerMatch)) { + // a STRING match which will be an exact match on the string supplied as the property validation key + bindCallback('valServerMatch', scope.valServerMatch, null); + } else { + throw 'valServerMatch value must be a string or a dictionary'; + } + scope.$on('$destroy', function () { + unsubscribe.forEach(function (u) { + return u(); + }); + }); + } + }; + } + angular.module('umbraco.directives.validation').directive('valServerMatch', valServerMatch); + 'use strict'; + /** + * @ngdoc directive + * @name umbraco.directives.directive:valSubView + * @restrict A + * @description Used to show validation warnings for a editor sub view (used in conjunction with: + * umb-editor-sub-view or umb-editor-sub-views) 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. + * When applied to **/ (function () { 'use strict'; + // Since this is a directive applied as an attribute, the value of that attribtue is the 'model' object property + // of the current inherited scope that the hasError/errorClass properties will apply to. + // This directive cannot have it's own scope because it's an attribute applied to another scoped directive. + // Due to backwards compatibility we can't really change this, ideally this would have it's own scope/properties. function valSubViewDirective() { - function controller($scope, $element) { + function controller($scope, $element, $attrs) { + var model = $scope.model; + // this is the default and required for backwards compat + if ($attrs && $attrs.valSubView) { + // get the property to use + model = $scope[$attrs.valSubView]; + } //expose api return { valStatusChanged: function valStatusChanged(args) { - // TODO: Verify this is correct, does $scope.model ever exist? - if ($scope.model) { + if (model) { if (!args.form.$valid) { var subViewContent = $element.find('.ng-invalid'); if (subViewContent.length > 0) { - $scope.model.hasError = true; - $scope.model.errorClass = args.showValidation ? 'show-validation' : null; + model.hasError = true; + model.errorClass = args.showValidation ? 'show-validation' : null; } else { - $scope.model.hasError = false; - $scope.model.errorClass = null; + model.hasError = false; + model.errorClass = null; } } else { - $scope.model.hasError = false; - $scope.model.errorClass = null; + model.hasError = false; + model.errorClass = null; } } } @@ -17295,22 +19935,28 @@ Use this directive to render a user group preview, where you can see the permiss } 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]) { + if (!ctrl[1]) { return; } + var model = scope.model; + // this is the default and required for backwards compat + if (attr && attr.valSubView) { + // get the property to use + model = scope[attr.valSubView]; + } var valFormManager = ctrl[1]; - scope.model.hasError = false; + model.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.model.hasError = true; + model.hasError = true; } else { - scope.model.hasError = false; + model.hasError = false; } } else { - scope.model.hasError = false; + model.hasError = false; } }); } @@ -17335,7 +19981,7 @@ Use this directive to render a user group preview, where you can see the permiss * @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() { + function valTab($timeout) { return { require: [ '^^form', @@ -17343,22 +19989,87 @@ Use this directive to render a user group preview, where you can see the permiss ], restrict: 'A', link: function link(scope, element, attr, ctrs) { - var valFormManager = ctrs[1]; - var tabAlias = scope.tab.alias; - scope.tabHasError = false; - //listen for form validation changes - valFormManager.onValidationStatusChanged(function (evt, args) { - if (!args.form.$valid) { - var tabContent = element.closest('.umb-editor').find('[data-element=\'tab-content-' + tabAlias + '\']'); - //check if the validation messages are contained inside of this tabs + var evts = []; + var form = ctrs[0]; + var tab = scope.$eval(attr.valTab) || scope.tab; + if (!tab) { + return; + } + var closestEditor = element.closest('.blockelement-inlineblock-editor'); + closestEditor = closestEditor.length === 0 ? element.closest('.umb-editor-sub-view') : closestEditor; + closestEditor = closestEditor.length === 0 ? element.closest('.umb-editor') : closestEditor; + setSuccess(); + function setValidity(form) { + var tabAlias = tab.alias || ''; + if (!form.$valid) { + var tabContent = closestEditor.find('[data-element=\'tab-content-' + tabAlias + '\']'); + //check if the validation messages are contained inside of this tabs if (tabContent.find('.ng-invalid').length > 0) { - scope.tabHasError = true; + setError(); } else { - scope.tabHasError = false; + setSuccess(); } } else { - scope.tabHasError = false; + setSuccess(); + } + } + function setError() { + scope.valTab_tabHasError = true; + tab.hasError = true; + } + function setSuccess() { + scope.valTab_tabHasError = false; + tab.hasError = false; + } + function subscribe() { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + try { + var _loop = function _loop() { + var control = _step.value; + unbind = scope.$watch(function () { + return control.$invalid; + }, function () { + setValidity(form); + }); + evts.push(unbind); + }; + for (var _iterator = form.$$controls[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var unbind; + _loop(); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return != null) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } } + } + function unsubscribe() { + evts.forEach(function (event) { + return event(); + }); + } + // we need to watch validation state on individual controls so we can update specific tabs accordingly + $timeout(function () { + scope.$watchCollection(function () { + return form.$$controls; + }, function (newValue) { + unsubscribe(); + subscribe(); + }); + }); + scope.$on('$destroy', function () { + unsubscribe(); }); } }; @@ -17378,90 +20089,1430 @@ Use this directive to render a user group preview, where you can see the permiss 'use strict'; (function () { 'use strict'; - /** - * A component to render the property action toggle - */ - function umbPropertyActionsController(keyboardService) { + angular.module('umbraco').component('umbBlockCard', { + template: '
      Other
      Other
      ', + controller: BlockCardController, + controllerAs: 'vm', + transclude: true, + bindings: { + blockConfigModel: '
      {{ group.label }}
      ', + controller: ElementEditorContentComponentController, + controllerAs: 'vm', + bindings: { model: '=' } + }); + function ElementEditorContentComponentController($scope, $filter, contentEditingHelper, contentTypeHelper) { + // We need a controller for the component to work. + var vm = this; + vm.tabs = []; + vm.activeTabAlias = null; + vm.getScope = getScope; + // used by property editors to get a scope that is the root of split view, content apps etc. + vm.setActiveTab = setActiveTab; + $scope.$watchCollection('vm.model.variants[0].tabs', function (newValue) { + contentTypeHelper.defineParentAliasOnGroups(newValue); + contentTypeHelper.relocateDisorientedGroups(newValue); + vm.tabs = $filter('filter')(newValue, function (tab) { + return tab.type === 1; + }); + if (vm.tabs.length > 0) { + // if we have tabs and some groups that doesn't belong to a tab we need to render those on an "Other" tab. + contentEditingHelper.registerGenericTab(newValue); + setActiveTab(vm.tabs[0]); + } + }); + function getScope() { + return $scope; } - function destroyDropDown() { - keyboardService.unbind('esc'); + function setActiveTab(tab) { + vm.activeTabAlias = tab.alias; + vm.tabs.forEach(function (tab) { + return tab.active = false; + }); + tab.active = true; } - vm.toggle = function () { - if (vm.isOpen === true) { - vm.close(); + } + }()); + 'use strict'; + (function () { + 'use strict'; + angular.module('umbraco').component('umbMediaCard', { + template: '

      {{vm.media.name}}
      ', + controller: MediaCardController, + controllerAs: 'vm', + transclude: true, + bindings: { + mediaKey: ' 0 && vm.allowedTypes.indexOf(vm.media.metaData.ContentTypeAlias) === -1; + if (vm.hasError === true || vm.notAllowed === true || vm.media && vm.media.trashed === true) { + $element.addClass('--hasError'); + vm.mediaCardForm.$setValidity('error', false); } else { - vm.open(); + $element.removeClass('--hasError'); + vm.mediaCardForm.$setValidity('error', true); } + } + vm.$onInit = function () { + unsubscribe.push($scope.$watchGroup([ + 'vm.media.trashed', + 'vm.hasError' + ], checkErrorState)); + vm.updateThumbnail(); + unsubscribe.push(eventsService.on('editors.media.saved', function (name, args) { + // if this media item uses the updated media type we want to reload the media file + if (args && args.media && args.media.key === vm.mediaKey) { + vm.updateThumbnail(); + } + })); }; - vm.open = function () { - vm.isOpen = true; - initDropDown(); + vm.$onDestroy = function () { + unsubscribe.forEach(function (x) { + return x(); + }); }; - vm.close = function () { - vm.isOpen = false; - destroyDropDown(); + vm.updateThumbnail = function () { + if (vm.mediaKey && vm.mediaKey !== '') { + vm.loading = true; + entityResource.getById(vm.mediaKey, 'Media').then(function (mediaEntity) { + vm.media = mediaEntity; + checkErrorState(); + vm.thumbnail = mediaHelper.resolveFileFromEntity(mediaEntity, true); + vm.loading = false; + }, function () { + localizationService.localize('mediaPicker_deletedItem').then(function (localized) { + vm.media = { + name: localized, + icon: 'icon-picture', + trashed: true + }; + vm.loading = false; + $element.addClass('--hasError'); + vm.mediaCardForm.$setValidity('error', false); + }); + }); + } }; - vm.executeAction = function (action) { - action.method(); - vm.close(); + } + }()); + 'use strict'; + (function () { + 'use strict'; + angular.module('umbraco').component('umbPropertyInfoButton', { + template: '
      ', + controller: UmbPropertyInfoButtonController, + controllerAs: 'vm', + transclude: true, + bindings: { + buttonTitle: '@?', + buttonTitleKey: '@?', + symbol: '@?' + } + }); + function UmbPropertyInfoButtonController(localizationService) { + var vm = this; + vm.show = false; + vm.onMouseClick = function ($event) { + vm.show = !vm.show; }; - vm.$onDestroy = function () { - if (vm.isOpen === true) { - destroyDropDown(); + vm.onMouseClickOutside = function ($event) { + vm.show = false; + }; + vm.$onInit = function () { + vm.symbol = vm.symbol || 'i'; + if (vm.buttonTitleKey) { + localizationService.localize(vm.buttonTitleKey).then(function (value) { + vm.buttonTitle = value; + }); } }; } - var umbPropertyActionsComponent = { - template: '
      ', - bindings: { actions: '<' }, + }()); + 'use strict'; + (function () { + 'use strict'; + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbBlockListBlock + * @description + * The component to render the view for a block. + * If a stylesheet is used then this uses a ShadowDom to make a scoped element. + * This way the backoffice styling does not collide with the block style. + */ + angular.module('umbraco').component('umbBlockListBlock', { + controller: BlockListBlockController, + controllerAs: 'model', + bindings: { + stylesheet: '@', + view: '@', + block: '=', + api: '<', + index: '<', + parentForm: '<' + }, + require: { valFormManager: '^^valFormManager' } + }); + function BlockListBlockController($scope, $compile, $element) { + var model = this; + model.$onInit = function () { + // This is ugly and is only necessary because we are not using components and instead + // relying on ng-include. It is definitely possible to compile the contents + // of the view into the DOM using $templateCache and $http instead of using + // ng - include which means that the controllerAs flows directly to the view. + // This would mean that any custom components would need to be updated instead of relying on $scope. + // Guess we'll leave it for now but means all things need to be copied to the $scope and then all + // primitives need to be watched. + // let the Block know about its form + model.block.setParentForm(model.parentForm); + // let the Block know about the current index + model.block.index = model.index; + $scope.block = model.block; + $scope.api = model.api; + $scope.index = model.index; + $scope.parentForm = model.parentForm; + $scope.valFormManager = model.valFormManager; + if (model.stylesheet) { + var shadowRoot = $element[0].attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '\n \n
      \n '); + $compile(shadowRoot)($scope); + } else { + $element.append($compile('
      ')($scope)); + } + }; + // We need to watch for changes on primitive types and upate the $scope values. + model.$onChanges = function (changes) { + if (changes.index) { + var index = changes.index.currentValue; + $scope.index = index; + // let the Block know about the current index: + model.block.index = index; + model.block.updateLabel(); + } + }; + } + }()); + 'use strict'; + function _typeof(obj) { + if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') { + _typeof = function _typeof(obj) { + return typeof obj; + }; + } else { + _typeof = function _typeof(obj) { + return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj; + }; + } + return _typeof(obj); + } + (function () { + 'use strict'; + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbBlockListPropertyEditor + * @function + * + * @description + * The component for the block list property editor. + */ + angular.module('umbraco').component('umbBlockListPropertyEditor', { + template: '
      Minimum %0% entries, needs %1% more.
      >
      Maximum %0% entries, %1% too many.
      ', + controller: BlockListController, controllerAs: 'vm', - controller: umbPropertyActionsController - }; - angular.module('umbraco.directives').component('umbPropertyActions', umbPropertyActionsComponent); + bindings: { model: '=' }, + require: { + propertyForm: '^form', + umbProperty: '?^umbProperty', + umbVariantContent: '?^^umbVariantContent', + umbVariantContentEditors: '?^^umbVariantContentEditors', + umbElementEditorContent: '?^^umbElementEditorContent' + } + }); + function BlockListController($scope, $timeout, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper, eventsService) { + var unsubscribe = []; + var modelObject; + // Property actions: + var copyAllBlocksAction = null; + var deleteAllBlocksAction = null; + var inlineEditing = false; + var liveEditing = true; + var vm = this; + vm.loading = true; + vm.currentBlockInFocus = null; + vm.setBlockFocus = function (block) { + if (vm.currentBlockInFocus !== null) { + vm.currentBlockInFocus.focus = false; + } + vm.currentBlockInFocus = block; + block.focus = true; + }; + vm.supportCopy = clipboardService.isSupported(); + vm.clipboardItems = []; + unsubscribe.push(eventsService.on('clipboardService.storageUpdate', updateClipboard)); + unsubscribe.push($scope.$on('editors.content.splitViewChanged', function (event, eventData) { + var compositeId = vm.umbVariantContent.editor.compositeId; + if (eventData.editors.some(function (x) { + return x.compositeId === compositeId; + })) { + updateAllBlockObjects(); + } + })); + vm.layout = []; + // The layout object specific to this Block Editor, will be a direct reference from Property Model. + vm.availableBlockTypes = []; + // Available block entries of this property editor. + vm.labels = {}; + localizationService.localizeMany([ + 'grid_addElement', + 'content_createEmpty' + ]).then(function (data) { + vm.labels.grid_addElement = data[0]; + vm.labels.content_createEmpty = data[1]; + }); + vm.$onInit = function () { + if (vm.umbProperty && !vm.umbVariantContent) { + // if we dont have vm.umbProperty, it means we are in the DocumentTypeEditor. + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain($scope, function (s) { + return s && s.vm && s.vm.constructor.name === 'umbVariantContentController'; + }); + vm.umbVariantContent = found ? found.vm : null; + if (!vm.umbVariantContent) { + throw 'Could not find umbVariantContent in the $scope chain'; + } + } + // set the onValueChanged callback, this will tell us if the block list model changed on the server + // once the data is submitted. If so we need to re-initialize + vm.model.onValueChanged = onServerValueChanged; + inlineEditing = vm.model.config.useInlineEditingAsDefault; + liveEditing = vm.model.config.useLiveEditing; + vm.validationLimit = vm.model.config.validationLimit; + vm.listWrapperStyles = {}; + if (vm.model.config.maxPropertyWidth) { + vm.listWrapperStyles['max-width'] = vm.model.config.maxPropertyWidth; + } + // We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated. + if (_typeof(vm.model.value) !== 'object' || vm.model.value === null) { + // testing if we have null or undefined value or if the value is set to another type than Object. + vm.model.value = {}; + } + var scopeOfExistence = $scope; + if (vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) { + scopeOfExistence = vm.umbVariantContentEditors.getScope(); + } else if (vm.umbElementEditorContent && vm.umbElementEditorContent.getScope) { + scopeOfExistence = vm.umbElementEditorContent.getScope(); + } + copyAllBlocksAction = { + labelKey: 'clipboard_labelForCopyAllEntries', + labelTokens: [vm.model.label], + icon: 'documents', + method: requestCopyAllBlocks, + isDisabled: true + }; + deleteAllBlocksAction = { + labelKey: 'clipboard_labelForRemoveAllEntries', + labelTokens: [], + icon: 'trash', + method: requestDeleteAllBlocks, + isDisabled: true + }; + var propertyActions = [ + copyAllBlocksAction, + deleteAllBlocksAction + ]; + if (vm.umbProperty) { + vm.umbProperty.setPropertyActions(propertyActions); + } + // Create Model Object, to manage our data for this Block Editor. + modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope); + modelObject.load().then(onLoaded); + }; + // Called when we save the value, the server may return an updated data and our value is re-synced + // we need to deal with that here so that our model values are all in sync so we basically re-initialize. + function onServerValueChanged(newVal, oldVal) { + // We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated. + if (_typeof(newVal) !== 'object' || newVal === null) { + // testing if we have null or undefined value or if the value is set to another type than Object. + vm.model.value = newVal = {}; + } + modelObject.update(vm.model.value, $scope); + onLoaded(); + } + function setDirty() { + if (vm.propertyForm) { + vm.propertyForm.$setDirty(); + } + } + function onLoaded() { + // Store a reference to the layout model, because we need to maintain this model. + vm.layout = modelObject.getLayout([]); + var invalidLayoutItems = []; + // Append the blockObjects to our layout. + vm.layout.forEach(function (entry) { + // $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject. + if (entry.$block === undefined || entry.$block === null || entry.$block.data === undefined) { + var block = getBlockObject(entry); + // If this entry was not supported by our property-editor it would return 'null'. + if (block !== null) { + entry.$block = block; + } else { + // then we need to filter this out and also update the underlying model. This could happen if the data + // is invalid for some reason or the data structure has changed. + invalidLayoutItems.push(entry); + } + } else { + updateBlockObject(entry.$block); + } + }); + // remove the ones that are invalid + invalidLayoutItems.forEach(function (entry) { + var index = vm.layout.findIndex(function (x) { + return x === entry; + }); + if (index >= 0) { + vm.layout.splice(index, 1); + } + }); + vm.availableContentTypesAliases = modelObject.getAvailableAliasesForBlockContent(); + vm.availableBlockTypes = modelObject.getAvailableBlocksForBlockPicker(); + updateClipboard(true); + vm.loading = false; + $scope.$evalAsync(); + } + function updateAllBlockObjects() { + // Update the blockObjects in our layout. + vm.layout.forEach(function (entry) { + // $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject. + if (entry.$block) { + updateBlockObject(entry.$block); + } + }); + } + function getDefaultViewForBlock(block) { + var defaultViewFolderPath = 'views/propertyeditors/blocklist/blocklistentryeditors/'; + if (block.config.unsupported === true) + return defaultViewFolderPath + 'unsupportedblock/unsupportedblock.editor.html'; + if (inlineEditing === true) + return defaultViewFolderPath + 'inlineblock/inlineblock.editor.html'; + return defaultViewFolderPath + 'labelblock/labelblock.editor.html'; + } + /** + * Ensure that the containing content variant languag and current property culture is transfered along + * to the scaffolded content object representing this block. + * This is required for validation along with ensuring that the umb-property inheritance is constently maintained. + * @param {any} content + */ + function ensureCultureData(content) { + if (!content) + return; + if (vm.umbVariantContent.editor.content.language) { + // set the scaffolded content's language to the language of the current editor + content.language = vm.umbVariantContent.editor.content.language; + } + // currently we only ever deal with invariant content for blocks so there's only one + content.variants[0].tabs.forEach(function (tab) { + tab.properties.forEach(function (prop) { + // set the scaffolded property to the culture of the containing property + prop.culture = vm.umbProperty.property.culture; + }); + }); + } + function getBlockObject(entry) { + var block = modelObject.getBlockObject(entry); + if (block === null) + return null; + block.view = block.config.view ? block.config.view : getDefaultViewForBlock(block); + block.showValidation = block.config.view ? true : false; + block.hideContentInOverlay = block.config.forceHideContentEditorInOverlay === true || inlineEditing === true; + block.showSettings = block.config.settingsElementTypeKey != null; + // If we have content, otherwise it doesn't make sense to copy. + block.showCopy = vm.supportCopy && block.config.contentElementTypeKey != null; + // Index is set by umbblocklistblock component and kept up to date by it. + block.index = 0; + block.setParentForm = function (parentForm) { + this._parentForm = parentForm; + }; + /** decorator methods, to enable switching out methods without loosing references that would have been made in Block Views codes */ + block.activate = function () { + this._activate(); + }; + block.edit = function () { + this._edit(); + }; + block.editSettings = function () { + this._editSettings(); + }; + block.requestDelete = function () { + this._requestDelete(); + }; + block.delete = function () { + this._delete(); + }; + block.copy = function () { + this._copy(); + }; + updateBlockObject(block); + return block; + } + /** As the block object now contains references to this instance of a property editor, we need to ensure that the Block Object contains latest references. + * This is a bit hacky but the only way to maintain this reference currently. + * Notice this is most relevant for invariant properties on variant documents, specially for the scenario where the scope of the reference we stored is destroyed, therefor we need to ensure we always have references to a current running property editor*/ + function updateBlockObject(block) { + ensureCultureData(block.content); + ensureCultureData(block.settings); + block._activate = activateBlock.bind(null, block); + block._edit = function () { + var blockIndex = vm.layout.indexOf(this.layout); + editBlock(this, false, blockIndex, this._parentForm); + }; + block._editSettings = function () { + var blockIndex = vm.layout.indexOf(this.layout); + editBlock(this, true, blockIndex, this._parentForm); + }; + block._requestDelete = requestDeleteBlock.bind(null, block); + block._delete = deleteBlock.bind(null, block); + block._copy = copyBlock.bind(null, block); + } + function addNewBlock(index, contentElementTypeKey) { + // Create layout entry. (not added to property model jet.) + var layoutEntry = modelObject.create(contentElementTypeKey); + if (layoutEntry === null) { + return false; + } + // make block model + var blockObject = getBlockObject(layoutEntry); + if (blockObject === null) { + return false; + } + // If we reach this line, we are good to add the layoutEntry and blockObject to our models. + // Add the Block Object to our layout entry. + layoutEntry.$block = blockObject; + // add layout entry at the decired location in layout. + vm.layout.splice(index, 0, layoutEntry); + // lets move focus to this new block. + vm.setBlockFocus(blockObject); + return true; + } + function deleteBlock(block) { + var layoutIndex = vm.layout.findIndex(function (entry) { + return entry.contentUdi === block.layout.contentUdi; + }); + if (layoutIndex === -1) { + throw new Error('Could not find layout entry of block with udi: ' + block.layout.contentUdi); + } + setDirty(); + var removed = vm.layout.splice(layoutIndex, 1); + removed.forEach(function (x) { + // remove any server validation errors associated + var guids = [ + udiService.getKey(x.contentUdi), + x.settingsUdi ? udiService.getKey(x.settingsUdi) : null + ]; + guids.forEach(function (guid) { + if (guid) { + serverValidationManager.removePropertyError(guid, vm.umbProperty.property.culture, vm.umbProperty.property.segment, '', { matchType: 'contains' }); + } + }); + }); + modelObject.removeDataAndDestroyModel(block); + } + function deleteAllBlocks() { + while (vm.layout.length) { + deleteBlock(vm.layout[0].$block); + } + ; + } + function activateBlock(blockObject) { + blockObject.active = true; + } + function editBlock(blockObject, openSettings, blockIndex, parentForm, options) { + options = options || {}; + // this must be set + if (blockIndex === undefined) { + throw 'blockIndex was not specified on call to editBlock'; + } + var wasNotActiveBefore = blockObject.active !== true; + // dont open the editor overlay if block has hidden its content editor in overlays and we are requesting to open content, not settings. + if (openSettings !== true && blockObject.hideContentInOverlay === true) { + return; + } + // if requesting to open settings but we dont have settings then return. + if (openSettings === true && !blockObject.config.settingsElementTypeKey) { + return; + } + activateBlock(blockObject); + // make a clone to avoid editing model directly. + var blockContentClone = Utilities.copy(blockObject.content); + var blockSettingsClone = null; + if (blockObject.config.settingsElementTypeKey) { + blockSettingsClone = Utilities.copy(blockObject.settings); + } + var blockEditorModel = { + $parentScope: $scope, + // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $parentForm: parentForm || vm.propertyForm, + // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) + hideContent: blockObject.hideContentInOverlay, + openSettings: openSettings === true, + createFlow: options.createFlow === true, + liveEditing: liveEditing, + title: blockObject.label, + view: 'views/common/infiniteeditors/blockeditor/blockeditor.html', + size: blockObject.config.editorSize || 'medium', + submit: function submit(blockEditorModel) { + if (liveEditing === false) { + // transfer values when submitting in none-liveediting mode. + blockObject.retrieveValuesFrom(blockEditorModel.content, blockEditorModel.settings); + } + blockObject.active = false; + editorService.close(); + }, + close: function close(blockEditorModel) { + if (blockEditorModel.createFlow) { + deleteBlock(blockObject); + } else { + if (liveEditing === true) { + // revert values when closing in liveediting mode. + blockObject.retrieveValuesFrom(blockContentClone, blockSettingsClone); + } + if (wasNotActiveBefore === true) { + blockObject.active = false; + } + } + editorService.close(); + } + }; + if (liveEditing === true) { + blockEditorModel.content = blockObject.content; + blockEditorModel.settings = blockObject.settings; + } else { + blockEditorModel.content = blockContentClone; + blockEditorModel.settings = blockSettingsClone; + } + // open property settings editor + editorService.open(blockEditorModel); + } + vm.requestShowCreate = requestShowCreate; + function requestShowCreate(createIndex, mouseEvent) { + if (vm.blockTypePicker) { + return; + } + if (vm.availableBlockTypes.length === 1) { + var wasAdded = false; + var blockType = vm.availableBlockTypes[0]; + wasAdded = addNewBlock(createIndex, blockType.blockConfigModel.contentElementTypeKey); + if (wasAdded && !(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + userFlowWhenBlockWasCreated(createIndex); + } + } else { + showCreateDialog(createIndex); + } + } + vm.requestShowClipboard = requestShowClipboard; + function requestShowClipboard(createIndex, mouseEvent) { + showCreateDialog(createIndex, true); + } + vm.showCreateDialog = showCreateDialog; + function showCreateDialog(createIndex, openClipboard) { + if (vm.blockTypePicker) { + return; + } + if (vm.availableBlockTypes.length === 0) { + return; + } + var amountOfAvailableTypes = vm.availableBlockTypes.length; + var blockPickerModel = { + $parentScope: $scope, + // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $parentForm: vm.propertyForm, + // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) + availableItems: vm.availableBlockTypes, + title: vm.labels.grid_addElement, + openClipboard: openClipboard, + orderBy: '$index', + view: 'views/common/infiniteeditors/blockpicker/blockpicker.html', + size: amountOfAvailableTypes > 8 ? 'medium' : 'small', + filter: amountOfAvailableTypes > 8, + clickPasteItem: function clickPasteItem(item, mouseEvent) { + if (Array.isArray(item.pasteData)) { + var indexIncrementor = 0; + item.pasteData.forEach(function (entry) { + if (requestPasteFromClipboard(createIndex + indexIncrementor, entry, item.type)) { + indexIncrementor++; + } + }); + } else { + requestPasteFromClipboard(createIndex, item.pasteData, item.type); + } + if (!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + blockPickerModel.close(); + } + }, + submit: function submit(blockPickerModel, mouseEvent) { + var wasAdded = false; + if (blockPickerModel && blockPickerModel.selectedItem) { + wasAdded = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentElementTypeKey); + } + if (!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + editorService.close(); + if (wasAdded) { + userFlowWhenBlockWasCreated(createIndex); + } + } + }, + close: function close() { + // if opned by a inline creator button(index less than length), we want to move the focus away, to hide line-creator. + if (createIndex < vm.layout.length) { + vm.setBlockFocus(vm.layout[Math.max(createIndex - 1, 0)].$block); + } + editorService.close(); + } + }; + blockPickerModel.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); + clipboardService.clearEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases); + }; + blockPickerModel.clipboardItems = vm.clipboardItems; + // open block picker overlay + editorService.open(blockPickerModel); + } + ; + function userFlowWhenBlockWasCreated(createIndex) { + if (vm.layout.length > createIndex) { + var blockObject = vm.layout[createIndex].$block; + if (inlineEditing === true) { + blockObject.activate(); + } else if (inlineEditing === false && blockObject.hideContentInOverlay !== true) { + blockObject.edit(); + } + } + } + function updateClipboard(firstTime) { + var oldAmount = vm.clipboardItems.length; + vm.clipboardItems = []; + var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); + entriesForPaste.forEach(function (entry) { + var pasteEntry = { + type: clipboardService.TYPES.ELEMENT_TYPE, + date: entry.date, + pasteData: entry.data, + elementTypeModel: { + name: entry.label, + icon: entry.icon + } + }; + if (Array.isArray(entry.data) === false) { + var scaffold = modelObject.getScaffoldFromAlias(entry.alias); + if (scaffold) { + pasteEntry.blockConfigModel = modelObject.getBlockConfiguration(scaffold.contentTypeKey); + } + } + blockPickerModel.clipboardItems.push(pasteEntry); + }); + var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases); + entriesForPaste.forEach(function (entry) { + var pasteEntry = { + type: clipboardService.TYPES.BLOCK, + date: entry.date, + pasteData: entry.data, + elementTypeModel: { + name: entry.label, + icon: entry.icon + } + }; + if (Array.isArray(entry.data) === false) { + pasteEntry.blockConfigModel = modelObject.getBlockConfiguration(entry.data.data.contentTypeKey); + } + vm.clipboardItems.push(pasteEntry); + }); + vm.clipboardItems.sort(function (a, b) { + return b.date - a.date; + }); + if (firstTime !== true && vm.clipboardItems.length > oldAmount) { + jumpClipboard(); + } + } + var jumpClipboardTimeout; + function jumpClipboard() { + if (jumpClipboardTimeout) { + return; + } + vm.jumpClipboardButton = true; + jumpClipboardTimeout = $timeout(function () { + vm.jumpClipboardButton = false; + jumpClipboardTimeout = null; + }, 2000); + } + function requestCopyAllBlocks() { + var aliases = []; + var elementTypesToCopy = vm.layout.filter(function (entry) { + return entry.$block.config.unsupported !== true; + }).map(function (entry) { + aliases.push(entry.$block.content.contentTypeAlias); + // No need to clone the data as its begin handled by the clipboardService. + return { + 'layout': entry.$block.layout, + 'data': entry.$block.data, + 'settingsData': entry.$block.settingsData + }; + }); + // remove duplicate aliases + aliases = aliases.filter(function (item, index) { + return aliases.indexOf(item) === index; + }); + var contentNodeName = '?'; + var contentNodeIcon = null; + if (vm.umbVariantContent) { + contentNodeName = vm.umbVariantContent.editor.content.name; + if (vm.umbVariantContentEditors) { + contentNodeIcon = vm.umbVariantContentEditors.content.icon.split(' ')[0]; + } else if (vm.umbElementEditorContent) { + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(' ')[0]; + } + } else if (vm.umbElementEditorContent) { + contentNodeName = vm.umbElementEditorContent.model.documentType.name; + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(' ')[0]; + } + localizationService.localize('clipboard_labelForArrayOfItemsFrom', [ + vm.model.label, + contentNodeName + ]).then(function (localizedLabel) { + clipboardService.copyArray(clipboardService.TYPES.BLOCK, aliases, elementTypesToCopy, localizedLabel, contentNodeIcon || 'icon-thumbnail-list', vm.model.id); + }); + } + ; + function copyBlock(block) { + clipboardService.copy(clipboardService.TYPES.BLOCK, block.content.contentTypeAlias, { + 'layout': block.layout, + 'data': block.data, + 'settingsData': block.settingsData + }, block.label, block.content.icon, block.content.udi); + } + function requestPasteFromClipboard(index, pasteEntry, pasteType) { + if (pasteEntry === undefined) { + return false; + } + var layoutEntry; + if (pasteType === clipboardService.TYPES.ELEMENT_TYPE) { + layoutEntry = modelObject.createFromElementType(pasteEntry); + } else if (pasteType === clipboardService.TYPES.BLOCK) { + layoutEntry = modelObject.createFromBlockData(pasteEntry); + } else { + // Not a supported paste type. + return false; + } + if (layoutEntry === null) { + // Pasting did not go well. + return false; + } + // make block model + var blockObject = getBlockObject(layoutEntry); + if (blockObject === null) { + // Initalization of the Block Object didnt go well, therefor we will fail the paste action. + return false; + } + // set the BlockObject on our layout entry. + layoutEntry.$block = blockObject; + // insert layout entry at the decired location in layout. + vm.layout.splice(index, 0, layoutEntry); + vm.currentBlockInFocus = blockObject; + return true; + } + function requestDeleteBlock(block) { + localizationService.localizeMany([ + 'general_delete', + 'blockEditor_confirmDeleteBlockMessage', + 'contentTypeEditor_yesDelete' + ]).then(function (data) { + var overlay = { + title: data[0], + content: localizationService.tokenReplace(data[1], [block.label]), + submitButtonLabel: data[2], + close: function close() { + overlayService.close(); + }, + submit: function submit() { + deleteBlock(block); + overlayService.close(); + } + }; + overlayService.confirmDelete(overlay); + }); + } + function requestDeleteAllBlocks() { + localizationService.localizeMany([ + 'content_nestedContentDeleteAllItems', + 'general_delete' + ]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function close() { + overlayService.close(); + }, + submit: function submit() { + deleteAllBlocks(); + overlayService.close(); + } + }); + }); + } + function openSettingsForBlock(block, blockIndex, parentForm) { + editBlock(block, true, blockIndex, parentForm); + } + vm.blockEditorApi = { + activateBlock: activateBlock, + editBlock: editBlock, + copyBlock: copyBlock, + requestDeleteBlock: requestDeleteBlock, + deleteBlock: deleteBlock, + openSettingsForBlock: openSettingsForBlock + }; + vm.sortableOptions = { + axis: 'y', + containment: 'parent', + cursor: 'grabbing', + handle: '.blockelement__draggable-element', + cancel: 'input,textarea,select,option', + classes: '.blockelement--dragging', + distance: 5, + tolerance: 'pointer', + scroll: true, + update: function update(ev, ui) { + setDirty(); + } + }; + function onAmountOfBlocksChanged() { + // enable/disable property actions + if (copyAllBlocksAction) { + copyAllBlocksAction.isDisabled = vm.layout.length === 0; + } + if (deleteAllBlocksAction) { + deleteAllBlocksAction.isDisabled = vm.layout.length === 0; + } + // validate limits: + if (vm.propertyForm && vm.validationLimit) { + var isMinRequirementGood = vm.validationLimit.min === null || vm.layout.length >= vm.validationLimit.min; + vm.propertyForm.minCount.$setValidity('minCount', isMinRequirementGood); + var isMaxRequirementGood = vm.validationLimit.max === null || vm.layout.length <= vm.validationLimit.max; + vm.propertyForm.maxCount.$setValidity('maxCount', isMaxRequirementGood); + } + } + unsubscribe.push($scope.$watch(function () { + return vm.layout.length; + }, onAmountOfBlocksChanged)); + $scope.$on('$destroy', function () { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + try { + for (var _iterator = unsubscribe[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var subscription = _step.value; + subscription(); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return != null) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + }); + } }()); 'use strict'; (function () { 'use strict'; - angular.module('umbraco').component('umbMiniSearch', { - template: ' ', - controller: UmbMiniSearchController, + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbBlockListRow + * @description + * renders each row for the block list editor + */ + angular.module('umbraco').component('umbBlockListRow', { + template: '
      ', + controller: BlockListRowController, controllerAs: 'vm', bindings: { - model: '=', - onStartTyping: '&?', - onSearch: '&?', - onBlur: '&?' + blockEditorApi: '<', + layout: '<', + index: '<' + }, + require: { valFormManager: '^^valFormManager' } + }); + function BlockListRowController($scope) { + var vm = this; + vm.$onInit = function () { + }; + } + }()); + 'use strict'; + (function () { + 'use strict'; + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbMediaPicker3PropertyEditor + * @function + * + * @description + * The component for the Media Picker property editor. + */ + angular.module('umbraco').component('umbMediaPicker3PropertyEditor', { + template: '
      Minimum %0% entries, needs %1% more.
      >
      Maximum %0% entries, %1% too many.
      ', + controller: MediaPicker3Controller, + controllerAs: 'vm', + bindings: { model: '=' }, + require: { + propertyForm: '^form', + umbProperty: '?^umbProperty', + umbVariantContent: '?^^umbVariantContent', + umbVariantContentEditors: '?^^umbVariantContentEditors', + umbElementEditorContent: '?^^umbElementEditorContent' } }); - function UmbMiniSearchController($scope) { + function MediaPicker3Controller($scope, editorService, clipboardService, localizationService, overlayService, userService, entityResource) { + var unsubscribe = []; + // Property actions: + var copyAllMediasAction = null; + var removeAllMediasAction = null; var vm = this; - var searchDelay = _.debounce(function () { - $scope.$apply(function () { - if (vm.onSearch) { - vm.onSearch(); + vm.loading = true; + vm.activeMediaEntry = null; + vm.supportCopy = clipboardService.isSupported(); + vm.addMediaAt = addMediaAt; + vm.editMedia = editMedia; + vm.removeMedia = removeMedia; + vm.copyMedia = copyMedia; + vm.labels = {}; + localizationService.localizeMany([ + 'grid_addElement', + 'content_createEmpty' + ]).then(function (data) { + vm.labels.grid_addElement = data[0]; + vm.labels.content_createEmpty = data[1]; + }); + vm.$onInit = function () { + vm.validationLimit = vm.model.config.validationLimit || {}; + // If single-mode we only allow 1 item as the maximum: + if (vm.model.config.multiple === false) { + vm.validationLimit.max = 1; + } + vm.model.config.crops = vm.model.config.crops || []; + vm.singleMode = vm.validationLimit.max === 1; + vm.allowedTypes = vm.model.config.filter ? vm.model.config.filter.split(',') : null; + copyAllMediasAction = { + labelKey: 'clipboard_labelForCopyAllEntries', + labelTokens: [vm.model.label], + icon: 'documents', + method: requestCopyAllMedias, + isDisabled: true + }; + removeAllMediasAction = { + labelKey: 'clipboard_labelForRemoveAllEntries', + labelTokens: [], + icon: 'trash', + method: requestRemoveAllMedia, + isDisabled: true + }; + var propertyActions = []; + if (vm.supportCopy) { + propertyActions.push(copyAllMediasAction); + } + propertyActions.push(removeAllMediasAction); + if (vm.umbProperty) { + vm.umbProperty.setPropertyActions(propertyActions); + } + if (vm.model.value === null || !Array.isArray(vm.model.value)) { + vm.model.value = []; + } + vm.model.value.forEach(function (mediaEntry) { + return updateMediaEntryData(mediaEntry); + }); + // set the onValueChanged callback, this will tell us if the media picker model changed on the server + // once the data is submitted. If so we need to re-initialize + vm.model.onValueChanged = onServerValueChanged; + userService.getCurrentUser().then(function (userData) { + if (!vm.model.config.startNodeId) { + if (vm.model.config.ignoreUserStartNodes === true) { + vm.model.config.startNodeId = -1; + vm.model.config.startNodeIsVirtual = true; + } else { + vm.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + vm.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; + } } + // only allow users to add and edit media if they have access to the media section + var hasAccessToMedia = userData.allowedSections.indexOf('media') !== -1; + vm.allowEdit = hasAccessToMedia; + vm.allowAdd = hasAccessToMedia; + vm.loading = false; }); - }, 500); - vm.onKeyDown = function (ev) { - //13: enter - switch (ev.keyCode) { - case 13: - if (vm.onSearch) { - vm.onSearch(); + }; + function onServerValueChanged(newVal, oldVal) { + if (newVal === null || !Array.isArray(newVal)) { + newVal = []; + vm.model.value = newVal; + } + vm.model.value.forEach(function (mediaEntry) { + return updateMediaEntryData(mediaEntry); + }); + } + function setDirty() { + if (vm.propertyForm) { + vm.propertyForm.$setDirty(); + } + if (vm.modelValueForm) { + vm.modelValueForm.modelValue.$setDirty(); + } + } + function addMediaAt(createIndex, $event) { + var mediaPicker = { + startNodeId: vm.model.config.startNodeId, + startNodeIsVirtual: vm.model.config.startNodeIsVirtual, + dataTypeKey: vm.model.dataTypeKey, + multiPicker: vm.singleMode !== true, + clickPasteItem: function clickPasteItem(item, mouseEvent) { + if (Array.isArray(item.data)) { + var indexIncrementor = 0; + item.data.forEach(function (entry) { + if (requestPasteFromClipboard(createIndex + indexIncrementor, entry, item.type)) { + indexIncrementor++; + } + }); + } else { + requestPasteFromClipboard(createIndex, item.data, item.type); + } + if (!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + mediaPicker.close(); + } + }, + submit: function submit(model) { + editorService.close(); + var indexIncrementor = 0; + model.selection.forEach(function (entry) { + var mediaEntry = {}; + mediaEntry.key = String.CreateGuid(); + mediaEntry.mediaKey = entry.key; + updateMediaEntryData(mediaEntry); + vm.model.value.splice(createIndex + indexIncrementor, 0, mediaEntry); + indexIncrementor++; + }); + setDirty(); + }, + close: function close() { + editorService.close(); } - break; + }; + if (vm.model.config.filter) { + mediaPicker.filter = vm.model.config.filter; + } + mediaPicker.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, vm.allowedTypes || null); + }; + mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, vm.allowedTypes || null); + mediaPicker.clipboardItems.sort(function (a, b) { + return b.date - a.date; + }); + editorService.mediaPicker(mediaPicker); + } + // To be used by infinite editor. (defined here cause we need configuration from property editor) + function changeMediaFor(mediaEntry, onSuccess) { + var mediaPicker = { + startNodeId: vm.model.config.startNodeId, + startNodeIsVirtual: vm.model.config.startNodeIsVirtual, + dataTypeKey: vm.model.dataTypeKey, + multiPicker: false, + submit: function submit(model) { + editorService.close(); + model.selection.forEach(function (entry) { + // only one. + mediaEntry.mediaKey = entry.key; + }); + // reset focal and crops: + mediaEntry.crops = null; + mediaEntry.focalPoint = null; + updateMediaEntryData(mediaEntry); + if (onSuccess) { + onSuccess(); + } + }, + close: function close() { + editorService.close(); + } + }; + if (vm.model.config.filter) { + mediaPicker.filter = vm.model.config.filter; + } + editorService.mediaPicker(mediaPicker); + } + function resetCrop(cropEntry) { + Object.assign(cropEntry, vm.model.config.crops.find(function (c) { + return c.alias === cropEntry.alias; + })); + cropEntry.coordinates = null; + setDirty(); + } + function updateMediaEntryData(mediaEntry) { + mediaEntry.crops = mediaEntry.crops || []; + mediaEntry.focalPoint = mediaEntry.focalPoint || { + left: 0.5, + top: 0.5 + }; + // Copy config and only transfer coordinates. + var newCrops = Utilities.copy(vm.model.config.crops); + newCrops.forEach(function (crop) { + var oldCrop = mediaEntry.crops.filter(function (x) { + return x.alias === crop.alias; + }).shift(); + if (oldCrop && oldCrop.height === crop.height && oldCrop.width === crop.width) { + crop.coordinates = oldCrop.coordinates; + } + }); + mediaEntry.crops = newCrops; + } + function removeMedia(media) { + var index = vm.model.value.indexOf(media); + if (index !== -1) { + vm.model.value.splice(index, 1); } + } + function deleteAllMedias() { + vm.model.value = []; + } + function setActiveMedia(mediaEntryOrNull) { + vm.activeMediaEntry = mediaEntryOrNull; + } + function editMedia(mediaEntry, options, $event) { + if ($event) + $event.stopPropagation(); + options = options || {}; + setActiveMedia(mediaEntry); + var documentInfo = getDocumentNameAndIcon(); + // make a clone to avoid editing model directly. + var mediaEntryClone = Utilities.copy(mediaEntry); + var mediaEditorModel = { + $parentScope: $scope, + // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $parentForm: vm.propertyForm, + // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) + createFlow: options.createFlow === true, + documentName: documentInfo.name, + mediaEntry: mediaEntryClone, + propertyEditor: { + changeMediaFor: changeMediaFor, + resetCrop: resetCrop + }, + enableFocalPointSetter: vm.model.config.enableLocalFocalPoint || false, + view: 'views/common/infiniteeditors/mediaEntryEditor/mediaEntryEditor.html', + size: 'large', + submit: function submit(model) { + vm.model.value[vm.model.value.indexOf(mediaEntry)] = mediaEntryClone; + setActiveMedia(null); + editorService.close(); + }, + close: function close(model) { + if (model.createFlow === true) { + } + setActiveMedia(null); + editorService.close(); + } + }; + // open property settings editor + editorService.open(mediaEditorModel); + } + var getDocumentNameAndIcon = function getDocumentNameAndIcon() { + // get node name + var contentNodeName = '?'; + var contentNodeIcon = null; + if (vm.umbVariantContent) { + contentNodeName = vm.umbVariantContent.editor.content.name; + if (vm.umbVariantContentEditors) { + contentNodeIcon = vm.umbVariantContentEditors.content.icon.split(' ')[0]; + } else if (vm.umbElementEditorContent) { + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(' ')[0]; + } + } else if (vm.umbElementEditorContent) { + contentNodeName = vm.umbElementEditorContent.model.documentType.name; + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(' ')[0]; + } + return { + name: contentNodeName, + icon: contentNodeIcon + }; }; - vm.onChange = function () { - if (vm.onStartTyping) { - vm.onStartTyping(); + var requestCopyAllMedias = function requestCopyAllMedias() { + var mediaKeys = vm.model.value.map(function (x) { + return x.mediaKey; + }); + entityResource.getByIds(mediaKeys, 'Media').then(function (entities) { + // gather aliases + var aliases = entities.map(function (mediaEntity) { + return mediaEntity.metaData.ContentTypeAlias; + }); + // remove duplicate aliases + aliases = aliases.filter(function (item, index) { + return aliases.indexOf(item) === index; + }); + var documentInfo = getDocumentNameAndIcon(); + localizationService.localize('clipboard_labelForArrayOfItemsFrom', [ + vm.model.label, + documentInfo.name + ]).then(function (localizedLabel) { + clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, vm.model.value, localizedLabel, documentInfo.icon || 'icon-thumbnail-list', vm.model.id); + }); + }); + }; + function copyMedia(mediaEntry) { + entityResource.getById(mediaEntry.mediaKey, 'Media').then(function (mediaEntity) { + clipboardService.copy(clipboardService.TYPES.MEDIA, mediaEntity.metaData.ContentTypeAlias, mediaEntry, mediaEntity.name, mediaEntity.icon, mediaEntry.key); + }); + } + function requestPasteFromClipboard(createIndex, pasteEntry, pasteType) { + if (pasteEntry === undefined) { + return false; + } + pasteEntry = clipboardService.parseContentForPaste(pasteEntry, pasteType); + pasteEntry.key = String.CreateGuid(); + updateMediaEntryData(pasteEntry); + vm.model.value.splice(createIndex, 0, pasteEntry); + return true; + } + function requestRemoveAllMedia() { + localizationService.localizeMany([ + 'mediaPicker_confirmRemoveAllMediaEntryMessage', + 'general_remove' + ]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function close() { + overlayService.close(); + }, + submit: function submit() { + deleteAllMedias(); + overlayService.close(); + } + }); + }); + } + vm.sortableOptions = { + cursor: 'grabbing', + handle: 'umb-media-card', + cancel: 'input,textarea,select,option', + classes: '.umb-media-card--dragging', + distance: 5, + tolerance: 'pointer', + scroll: true, + update: function update(ev, ui) { + setDirty(); } - searchDelay(); }; + function onAmountOfMediaChanged() { + // enable/disable property actions + if (copyAllMediasAction) { + copyAllMediasAction.isDisabled = vm.model.value.length === 0; + } + if (removeAllMediasAction) { + removeAllMediasAction.isDisabled = vm.model.value.length === 0; + } + // validate limits: + if (vm.propertyForm && vm.validationLimit) { + var isMinRequirementGood = vm.validationLimit.min === null || vm.model.value.length >= vm.validationLimit.min; + vm.propertyForm.minCount.$setValidity('minCount', isMinRequirementGood); + var isMaxRequirementGood = vm.validationLimit.max === null || vm.model.value.length <= vm.validationLimit.max; + vm.propertyForm.maxCount.$setValidity('maxCount', isMaxRequirementGood); + } + } + unsubscribe.push($scope.$watch(function () { + return vm.model.value.length; + }, onAmountOfMediaChanged)); + $scope.$on('$destroy', function () { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + try { + for (var _iterator = unsubscribe[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var subscription = _step.value; + subscription(); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return != null) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + }); } }()); }()); \ No newline at end of file diff --git a/TestSite/Umbraco/Js/umbraco.filters.js b/TestSite/Umbraco/Js/umbraco.filters.js index 180a0a4..425516b 100644 --- a/TestSite/Umbraco/Js/umbraco.filters.js +++ b/TestSite/Umbraco/Js/umbraco.filters.js @@ -2,12 +2,33 @@ 'use strict'; angular.module('umbraco.filters', []); 'use strict'; + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); + } + function _nonIterableSpread() { + throw new TypeError('Invalid attempt to spread non-iterable instance'); + } + function _iterableToArray(iter) { + if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === '[object Arguments]') + return Array.from(iter); + } + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } + } angular.module('umbraco.filters').filter('compareArrays', function () { return function inArray(array, compareArray, compareProperty) { + if (!compareArray || !compareArray.length) { + return _toConsumableArray(array); + } var result = []; - angular.forEach(array, function (arrayItem) { + array.forEach(function (arrayItem) { var exists = false; - angular.forEach(compareArray, function (compareItem) { + compareArray.forEach(function (compareItem) { if (arrayItem[compareProperty] === compareItem[compareProperty]) { exists = true; } @@ -169,7 +190,7 @@ 'use strict'; /** * @ngdoc filter - * @name umbraco.filters.filter:CMS_joinArray + * @name umbraco.filters.filter:umbCmsJoinArray * @namespace umbCmsJoinArray * * param {array} array of string or objects, if an object use the third argument to specify which prop to list. @@ -182,7 +203,7 @@ */ angular.module('umbraco.filters').filter('umbCmsJoinArray', function () { return function join(array, separator, prop) { - return (!angular.isUndefined(prop) ? array.map(function (item) { + return (!Utilities.isUndefined(prop) ? array.map(function (item) { return item[prop]; }) : array).join(separator || ''); }; @@ -190,6 +211,25 @@ 'use strict'; /** * @ngdoc filter + * @name umbraco.filters.filter:umbCmsTitleCase + * @namespace umbCmsTitleCase + * + * param {string} the text turned into title case. + * + * @description + * Transforms text to title case. Capitalizes the first letter of each word, and transforms the rest of the word to lower case. + * + */ + angular.module('umbraco.filters').filter('umbCmsTitleCase', function () { + return function (str) { + return str.replace(/\w\S*/g, function (txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + }); + }; + }); + 'use strict'; + /** + * @ngdoc filter * @name umbraco.filters.filter:umbWordLimit * @namespace umbWordLimitFilter * @@ -200,10 +240,10 @@ 'use strict'; function umbWordLimitFilter() { return function (collection, property) { - if (!angular.isString(collection)) { + if (!Utilities.isString(collection)) { return collection; } - if (angular.isUndefined(property)) { + if (Utilities.isUndefined(property)) { return collection; } var newString = ''; diff --git a/TestSite/Umbraco/Js/umbraco.installer.js b/TestSite/Umbraco/Js/umbraco.installer.js index ff9aafb..73009bd 100644 --- a/TestSite/Umbraco/Js/umbraco.installer.js +++ b/TestSite/Umbraco/Js/umbraco.installer.js @@ -38,14 +38,14 @@ angular.module('umbraco.install').factory('installerService', function ($rootScope, $q, $timeout, $http, $templateRequest) { var _status = { index: 0, - current: undefined, - steps: undefined, + current: null, + steps: null, loading: true, progress: '100%' }; - var factTimer = undefined; + var factTimer; var _installerModel = { - installId: undefined, + installId: null, instructions: {} }; //add to umbraco installer facts here @@ -54,8 +54,8 @@ 'Over 500 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.com', + '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.com', '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', @@ -65,7 +65,7 @@ '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 700 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden', + 'More than 700 people from all over the world meet each year in Denmark in May 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' @@ -113,11 +113,11 @@ /** 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)) { + if (Utilities.isFunction(fn)) { fn(); } } else { - if (angular.isFunction(fn)) { + if (Utilities.isFunction(fn)) { scope.$apply(fn); } else { scope.$apply(); @@ -298,7 +298,7 @@ }); }, switchToFeedback: function switchToFeedback() { - service.status.current = undefined; + service.status.current = null; service.status.loading = true; service.status.configuring = false; //initial fact @@ -311,8 +311,8 @@ switchToConfiguration: function switchToConfiguration() { service.status.loading = false; service.status.configuring = true; - service.status.feedback = undefined; - service.status.fact = undefined; + service.status.feedback = null; + service.status.fact = null; if (factTimer) { clearInterval(factTimer); } @@ -354,7 +354,7 @@ id: -1 } ]; - if (installerService.status.current.model.dbType === undefined) { + if (Utilities.isUndefined(installerService.status.current.model.dbType) || installerService.status.current.model.dbType === null) { installerService.status.current.model.dbType = 0; } $scope.validateAndForward = function () { diff --git a/TestSite/Umbraco/Js/umbraco.interceptors.js b/TestSite/Umbraco/Js/umbraco.interceptors.js index dfa306e..0aa6f54 100644 --- a/TestSite/Umbraco/Js/umbraco.interceptors.js +++ b/TestSite/Umbraco/Js/umbraco.interceptors.js @@ -8,6 +8,7 @@ $httpProvider.defaults.xsrfCookieName = 'UMB-XSRF-TOKEN'; $httpProvider.interceptors.push('securityInterceptor'); $httpProvider.interceptors.push('debugRequestInterceptor'); + $httpProvider.interceptors.push('requiredHeadersInterceptor'); $httpProvider.interceptors.push('doNotPostDollarVariablesOnPostRequestInterceptor'); $httpProvider.interceptors.push('cultureRequestInterceptor'); } @@ -35,6 +36,7 @@ if ($routeParams) { // it's an API request, add the current client culture as a header value config.headers['X-UMB-CULTURE'] = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + config.headers['X-UMB-SEGMENT'] = $routeParams.csegment ? $routeParams.csegment : null; } return config; } @@ -99,7 +101,7 @@ //dealing with requests: 'request': function request(config) { if (config.method === 'POST') { - var clone = angular.copy(config); + var clone = Utilities.copy(config); transform(clone.data); return clone; } @@ -122,6 +124,29 @@ (function () { 'use strict'; /** + * Used to set required headers on all requests where necessary + * @param {any} $q + * @param {any} urlHelper + */ + function requiredHeadersInterceptor($q, urlHelper) { + return { + //dealing with requests: + 'request': function request(config) { + // This is a standard header that should be sent for all ajax requests and is required for + // how the server handles auth rejections, etc... see + // https://github.com/aspnet/AspNetKatana/blob/e2b18ec84ceab7ffa29d80d89429c9988ab40144/src/Microsoft.Owin.Security.Cookies/Provider/DefaultBehavior.cs + // https://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/ + config.headers['X-Requested-With'] = 'XMLHttpRequest'; + return config; + } + }; + } + angular.module('umbraco.interceptors').factory('requiredHeadersInterceptor', requiredHeadersInterceptor); + }()); + 'use strict'; + (function () { + 'use strict'; + /** * This http interceptor listens for authentication successes and failures * @param {any} $q * @param {any} $injector @@ -156,7 +181,7 @@ // 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(rejection.status) !== -1) { + if (headers['x-umb-ignore-error'] === 'ignore' || config.umbIgnoreErrors === true || Utilities.isArray(config.umbIgnoreStatus) && config.umbIgnoreStatus.indexOf(rejection.status) !== -1) { //exit/ignore return $q.reject(rejection); } diff --git a/TestSite/Umbraco/Js/umbraco.preview.js b/TestSite/Umbraco/Js/umbraco.preview.js index 49e6231..8716543 100644 --- a/TestSite/Umbraco/Js/umbraco.preview.js +++ b/TestSite/Umbraco/Js/umbraco.preview.js @@ -6,7 +6,7 @@ var app = angular.module('umbraco.preview', [ 'umbraco.resources', 'umbraco.services' - ]).controller('previewController', function ($scope, $window, $location) { + ]).controller('previewController', function ($scope, $window, $location, $http) { $scope.currentCulture = { iso: '', title: '...', @@ -14,7 +14,7 @@ }; var cultures = []; $scope.tabbingActive = false; - // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { if (evt.keyCode === 9) { @@ -30,7 +30,30 @@ window.removeEventListener('mousedown', disableTabbingActive); window.addEventListener('keydown', handleFirstTab); } + var iframeWrapper = angular.element('#demo-iframe-wrapper'); + var canvasDesignerPanel = angular.element('#canvasdesignerPanel'); window.addEventListener('keydown', handleFirstTab); + window.addEventListener('resize', scaleIframeWrapper); + iframeWrapper.on('transitionend', scaleIframeWrapper); + function scaleIframeWrapper() { + if ($scope.previewDevice.name == 'fullsize') { + // dont scale fullsize preview + iframeWrapper.css({ 'transform': '' }); + } else { + var wrapWidth = canvasDesignerPanel.width(); + // width of the wrapper + var wrapHeight = canvasDesignerPanel.height(); + var childWidth = iframeWrapper.width() + 30; + // width of child iframe plus some space + var childHeight = iframeWrapper.height() + 30; + // child height plus some space + var wScale = wrapWidth / childWidth; + var hScale = wrapHeight / childHeight; + var scale = Math.min(wScale, hScale, 1); + // get the lowest ratio, but not higher than 1 + iframeWrapper.css({ 'transform': 'scale(' + scale + ')' }); // set scale + } + } //gets a real query string value function getParameterByName(name, url) { if (!url) @@ -79,11 +102,19 @@ console.log('Could not connect to SignalR preview hub.'); }); } + function fixExternalLinks(iframe) { + // Make sure external links don't open inside the iframe + Array.from(iframe.contentDocument.getElementsByTagName('a')).filter(function (a) { + return a.hostname !== location.hostname && !a.target; + }).forEach(function (a) { + return a.target = '_top'; + }); + } var isInit = getParameterByName('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. + //this flag. This is a required trick to get around chrome popup mgr. return; } setPageUrl(); @@ -95,7 +126,7 @@ name: 'fullsize', css: 'fullsize', icon: 'icon-application-window-alt', - title: 'Browser' + title: 'Fit browser' }, { name: 'desktop', @@ -162,10 +193,23 @@ closeOthers(); $scope.$digest(); } - var win = angular.element($window); - win.on('blur', windowBlurHandler); + window.addEventListener('blur', windowBlurHandler); + function windowVisibilityHandler(e) { + var amountOfPreviewSessions = localStorage.getItem('UmbPreviewSessionAmount'); + // When tab is visible again: + if (document.hidden === false) { + checkPreviewState(); + } + } + document.addEventListener('visibilitychange', windowVisibilityHandler); + function beforeUnloadHandler(e) { + endPreviewSession(); + } + window.addEventListener('beforeunload', beforeUnloadHandler, false); $scope.$on('$destroy', function () { - win.off('blur', handleBlwindowBlurHandlerur); + window.removeEventListener('blur', windowBlurHandler); + document.removeEventListener('visibilitychange', windowVisibilityHandler); + window.removeEventListener('beforeunload', beforeUnloadHandler); }); function setPageUrl() { $scope.pageId = $location.search().id || getParameterByName('id'); @@ -178,6 +222,113 @@ $scope.pageUrl = 'frame?' + query; } } + function getCookie(cname) { + var name = cname + '='; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return null; + } + function setCookie(cname, cvalue, exminutes) { + var d = new Date(); + d.setTime(d.getTime() + exminutes * 60 * 1000); + document.cookie = cname + '=' + cvalue + ';expires=' + d.toUTCString() + ';path=/'; + } + var hasPreviewDialog = false; + function checkPreviewState() { + if (getCookie('UMB_PREVIEW') === null) { + if (hasPreviewDialog === true) + return; + hasPreviewDialog = true; + // Ask to re-enter preview mode? + var localizeVarsFallback = { + 'returnToPreviewHeadline': 'Preview website?', + 'returnToPreviewDescription': 'You have ended preview mode, do you want to enable it again to view the latest saved version of your website?', + 'returnToPreviewAcceptButton': 'Preview latest version', + 'returnToPreviewDeclineButton': 'View published version' + }; + var umbLocalizedVars = Object.assign(localizeVarsFallback, $window.umbLocalizedVars); + // This modal is also used in websitepreview.js + var modelStyles = '\n\n /* Webfont: LatoLatin-Bold */\n @font-face {\n font-family: \'Lato\';\n src: url(\'https://fonts.googleapis.com/css2?family=Lato:wght@700&display=swap\');\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n text-rendering: optimizeLegibility;\n }\n\n .umbraco-preview-dialog {\n position: fixed;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 99999999;\n top:0;\n bottom:0;\n left:0;\n right:0;\n overflow: auto;\n background-color: rgba(0,0,0,0.6);\n }\n\n .umbraco-preview-dialog__modal {\n background-color: #fff;\n border-radius: 6px;\n box-shadow: 0 3px 7px rgba(0,0,0,0.3);\n margin: auto;\n padding: 30px 40px;\n width: 100%;\n max-width: 540px;\n font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif;\n font-size: 15px;\n }\n\n .umbraco-preview-dialog__headline {\n font-weight: 700;\n font-size: 22px;\n color: #1b264f;\n margin-top:10px;\n margin-bottom:20px;\n }\n .umbraco-preview-dialog__question {\n margin-bottom:30px;\n }\n .umbraco-preview-dialog__modal > button {\n display: inline-block;\n cursor: pointer;\n padding: 8px 18px;\n text-align: center;\n vertical-align: middle;\n border-radius: 3px;\n border:none;\n font-family: inherit;\n font-weight: 700;\n font-size: 15px;\n float:right;\n margin-left:10px;\n\n color: #1b264f;\n background-color: #f6f1ef;\n }\n .umbraco-preview-dialog__modal > button:hover {\n color: #2152a3;\n background-color: #f6f1ef;\n }\n .umbraco-preview-dialog__modal > button.umbraco-preview-dialog__continue {\n color: #fff;\n background-color: #2bc37c;\n }\n .umbraco-preview-dialog__modal > button.umbraco-preview-dialog__continue:hover {\n background-color: #39d38b;\n }\n '; + var bodyEl = document.getElementsByTagName('BODY')[0]; + var fragment = document.createElement('div'); + var shadowRoot = fragment.attachShadow({ mode: 'open' }); + var style = document.createElement('style'); + style.innerHTML = modelStyles; + shadowRoot.appendChild(style); + var con = document.createElement('div'); + con.className = 'umbraco-preview-dialog'; + shadowRoot.appendChild(con); + var modal = document.createElement('div'); + modal.className = 'umbraco-preview-dialog__modal'; + modal.innerHTML = '
      '.concat(umbLocalizedVars.returnToPreviewHeadline, '
      \n
      ').concat(umbLocalizedVars.returnToPreviewDescription, '
      '); + con.appendChild(modal); + var declineButton = document.createElement('button'); + declineButton.type = 'button'; + declineButton.innerHTML = umbLocalizedVars.returnToPreviewDeclineButton; + declineButton.addEventListener('click', function () { + bodyEl.removeChild(fragment); + $scope.exitPreview(); + hasPreviewDialog = false; + }); + modal.appendChild(declineButton); + var continueButton = document.createElement('button'); + continueButton.type = 'button'; + continueButton.className = 'umbraco-preview-dialog__continue'; + continueButton.innerHTML = umbLocalizedVars.returnToPreviewAcceptButton; + continueButton.addEventListener('click', function () { + bodyEl.removeChild(fragment); + reenterPreviewMode(); + hasPreviewDialog = false; + }); + modal.appendChild(continueButton); + bodyEl.appendChild(fragment); + continueButton.focus(); + } + } + function reenterPreviewMode() { + //Re-enter Preview Mode + $http({ + method: 'POST', + url: '../preview/enterPreview', + params: { id: $scope.pageId } + }); + startPreviewSession(); + } + function getPageURL() { + var culture = $location.search().culture || getParameterByName('culture'); + var relativeUrl = '/' + $scope.pageId; + if (culture) { + relativeUrl += '?culture=' + culture; + } + return relativeUrl; + } + function startPreviewSession() { + // lets registrer this preview session. + var amountOfPreviewSessions = Math.max(localStorage.getItem('UmbPreviewSessionAmount') || 0, 0); + amountOfPreviewSessions++; + localStorage.setItem('UmbPreviewSessionAmount', amountOfPreviewSessions); + } + function resetPreviewSessions() { + localStorage.setItem('UmbPreviewSessionAmount', 0); + } + function endPreviewSession() { + var amountOfPreviewSessions = localStorage.getItem('UmbPreviewSessionAmount') || 0; + amountOfPreviewSessions--; + localStorage.setItem('UmbPreviewSessionAmount', amountOfPreviewSessions); + if (amountOfPreviewSessions <= 0) { + // We are good to end preview mode. + navigator.sendBeacon('../preview/end'); + } + } + startPreviewSession(); /*****************************************************************************/ /* Preview devices */ /*****************************************************************************/ @@ -186,19 +337,23 @@ $scope.previewDevice = device; }; /*****************************************************************************/ + /* Open website in preview mode */ + /*****************************************************************************/ + $scope.openInBrowser = function () { + setCookie('UMB-WEBSITE-PREVIEW-ACCEPT', 'true', 5); + window.open(getPageURL(), '_blank'); + }; + /*****************************************************************************/ /* Exit Preview */ /*****************************************************************************/ $scope.exitPreview = function () { - var culture = $location.search().culture || getParameterByName('culture'); - var relativeUrl = '/' + $scope.pageId; - if (culture) { - relativeUrl += '?culture=' + culture; - } - window.top.location.href = '../preview/end?redir=' + encodeURIComponent(relativeUrl); + resetPreviewSessions(); + window.top.location.href = '../preview/end?redir=' + encodeURIComponent(getPageURL()); }; $scope.onFrameLoaded = function (iframe) { $scope.frameLoaded = true; configureSignalR(iframe); + fixExternalLinks(iframe); $scope.currentCultureIso = $location.search().culture || null; }; /*****************************************************************************/ @@ -243,8 +398,8 @@ controller: function controller($element, $scope, angularHelper) { var vm = this; vm.$postLink = function () { - var resultFrame = $element.find('#resultFrame'); - resultFrame.on('load', iframeReady); + var resultFrame = $element.find('#resultFrame').get(0); + resultFrame.addEventListener('load', iframeReady); }; function iframeReady() { var iframe = $element.find('#resultFrame').get(0); diff --git a/TestSite/Umbraco/Js/umbraco.resources.js b/TestSite/Umbraco/Js/umbraco.resources.js index 6d28975..d05f07a 100644 --- a/TestSite/Umbraco/Js/umbraco.resources.js +++ b/TestSite/Umbraco/Js/umbraco.resources.js @@ -15,12 +15,69 @@ */ function authResource($q, $http, umbRequestHelper, angularHelper) { return { + /** + * @ngdoc method + * @name umbraco.resources.authResource#get2FAProviders + * @methodOf umbraco.resources.authResource + * + * @description + * Logs the Umbraco backoffice user in if the credentials are good + * + * ##usage + *
      +    * authResource.get2FAProviders()
      +    *    .then(function(data) {
      +    *        //Do stuff ...
      +    *    });
      +    * 
      + * @returns {Promise} resourcePromise object + * + */ get2FAProviders: function get2FAProviders() { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'Get2FAProviders')), 'Could not retrive two factor provider info'); }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#get2FAProviders + * @methodOf umbraco.resources.authResource + * + * @description + * Generate the two-factor authentication code for the provider and send it to the user + * + * ##usage + *
      +    * authResource.send2FACode(provider)
      +    *    .then(function(data) {
      +    *        //Do stuff ...
      +    *    });
      +    * 
      + * @param {string} provider Name of the provider + * @returns {Promise} resourcePromise object + * + */ send2FACode: function send2FACode(provider) { - return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostSend2FACode'), angular.toJson(provider)), 'Could not send code'); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostSend2FACode'), Utilities.toJson(provider)), 'Could not send code'); }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#get2FAProviders + * @methodOf umbraco.resources.authResource + * + * @description + * Verify the two-factor authentication code entered by the user against the provider + * + * ##usage + *
      +    * authResource.verify2FACode(provider, code)
      +    *    .then(function(data) {
      +    *        //Do stuff ...
      +    *    });
      +    * 
      + * @param {string} provider Name of the provider + * @param {string} code The two-factor authentication code + * @returns {Promise} resourcePromise object + * + */ verify2FACode: function verify2FACode(provider, code) { return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'PostVerify2FACode'), { code: code, @@ -604,9 +661,48 @@ }); } return { + /** + * @ngdoc method + * @name umbraco.resources.contentResource#allowsCultureVariation + * @methodOf umbraco.resources.contentResource + * + * @description + * Check whether any content types have culture variant enabled + * + * ##usage + *
      +    * contentResource.allowsCultureVariation()
      +    *    .then(function() {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @returns {Promise} resourcePromise object. + * + */ allowsCultureVariation: function allowsCultureVariation() { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'AllowsCultureVariation')), 'Failed to retrieve variant content types'); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#savePermissions + * @methodOf umbraco.resources.contentResource + * + * @description + * Save user group permissions for the content + * + * ##usage + *
      +    * contentResource.savePermissions(saveModel)
      +    *    .then(function() {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @param {object} The object which contains the user group permissions for the content + * @returns {Promise} resourcePromise object. + * + */ savePermissions: function savePermissions(saveModel) { if (!saveModel) { throw 'saveModel cannot be null'; @@ -619,6 +715,25 @@ } return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSaveUserGroupPermissions'), saveModel), 'Failed to save permissions'); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getRecycleBin + * @methodOf umbraco.resources.contentResource + * + * @description + * Get the recycle bin + * + * ##usage + *
      +    * contentResource.getRecycleBin()
      +    *    .then(function() {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @returns {Promise} resourcePromise object. + * + */ getRecycleBin: function getRecycleBin() { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetRecycleBin')), 'Failed to retrieve data for content recycle bin'); }, @@ -839,6 +954,26 @@ deleteById: function deleteById(id) { return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to delete item ' + id); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#deleteBlueprint + * @methodOf umbraco.resources.contentResource + * + * @description + * Deletes a content blueprint item with a given id + * + * ##usage + *
      +    * contentResource.deleteBlueprint(1234)
      +    *    .then(function() {
      +    *        alert('its gone!');
      +    *    });
      +    * 
      + * + * @param {Int} id id of content blueprint item to delete + * @returns {Promise} resourcePromise object. + * + */ deleteBlueprint: function deleteBlueprint(id) { return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'DeleteBlueprint', [{ id: id }])), 'Failed to delete blueprint ' + id); }, @@ -869,14 +1004,75 @@ return $q.when(umbDataFormatter.formatContentGetData(result)); }); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getBlueprintById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets a content blueprint item with a given id + * + * ##usage + *
      +    * contentResource.getBlueprintById(1234)
      +    *    .then(function() {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @param {Int} id id of content blueprint item to retrieve + * @returns {Promise} resourcePromise object. + * + */ getBlueprintById: function getBlueprintById(id) { - return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetBlueprintById', [{ id: id }])), 'Failed to retrieve data for content id ' + id).then(function (result) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetBlueprintById', { id: id })), 'Failed to retrieve data for content id ' + id).then(function (result) { return $q.when(umbDataFormatter.formatContentGetData(result)); }); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getNotifySettingsById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets notification options for a content item with a given id for the current user + * + * ##usage + *
      +    * contentResource.getNotifySettingsById(1234)
      +    *    .then(function() {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @param {Int} id id of content item + * @returns {Promise} resourcePromise object. + * + */ getNotifySettingsById: function getNotifySettingsById(id) { - return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetNotificationOptions', [{ contentId: id }])), 'Failed to retrieve data for content id ' + id); + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetNotificationOptions', { contentId: id })), 'Failed to retrieve data for content id ' + id); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getNotifySettingsById + * @methodOf umbraco.resources.contentResource + * + * @description + * Sets notification settings for a content item with a given id for the current user + * + * ##usage + *
      +    * contentResource.setNotifySettingsById(1234,["D", "F", "H"])
      +    *    .then(function() {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @param {Int} id id of content item + * @param {Array} options the notification options to set for the content item + * @returns {Promise} resourcePromise object. + * + */ setNotifySettingsById: function setNotifySettingsById(id, options) { if (!id) { throw 'contentId cannot be null'; @@ -909,13 +1105,13 @@ */ getByIds: function getByIds(ids) { var idQuery = ''; - _.each(ids, function (item) { - idQuery += 'ids=' + item + '&'; + ids.forEach(function (id) { + return idQuery += 'ids='.concat(id, '&'); }); return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetByIds', idQuery)), 'Failed to retrieve data for content with multiple ids').then(function (result) { //each item needs to be re-formatted - _.each(result, function (r) { - umbDataFormatter.formatContentGetData(r); + result.forEach(function (r) { + return umbDataFormatter.formatContentGetData(r); }); return $q.when(result); }); @@ -953,18 +1149,80 @@ * */ getScaffold: function getScaffold(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).then(function (result) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetEmpty', { + contentTypeAlias: alias, + parentId: parentId + })), 'Failed to retrieve data for empty content item type ' + alias).then(function (result) { + return $q.when(umbDataFormatter.formatContentGetData(result)); + }); + }, + getScaffolds: function getScaffolds(parentId, aliases) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetEmptyByAliases'), { + parentId: parentId, + contentTypeAliases: aliases + }), 'Failed to retrieve data for empty content item aliases ' + aliases.join(', ')).then(function (result) { + Object.keys(result).map(function (key) { + result[key] = umbDataFormatter.formatContentGetData(result[key]); + }); + return $q.when(result); + }); + }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getScaffoldByKey + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. + * + * - Parent Id must be provided so umbraco knows where to store the content + * - Content Type Id must be provided so umbraco knows which properties to put on the content scaffold + * + * The scaffold is used to build editors for content that has not yet been populated with data. + * + * ##usage + *
      +     * contentResource.getScaffoldByKey(1234, '...')
      +     *    .then(function(scaffold) {
      +     *        var myDoc = scaffold;
      +      *        myDoc.name = "My new document";
      +     *
      +     *        contentResource.publish(myDoc, true)
      +     *            .then(function(content){
      +     *                alert("Retrieved, updated and published again");
      +     *            });
      +     *    });
      +      * 
      + * + * @param {Int} parentId id of content item to return + * @param {String} contentTypeGuid contenttype guid to base the scaffold on + * @returns {Promise} resourcePromise object containing the content scaffold. + * + */ + getScaffoldByKey: function getScaffoldByKey(parentId, contentTypeKey) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetEmptyByKey', { + contentTypeKey: contentTypeKey, + parentId: parentId + })), 'Failed to retrieve data for empty content item id ' + contentTypeKey).then(function (result) { return $q.when(umbDataFormatter.formatContentGetData(result)); }); }, + getScaffoldByKeys: function getScaffoldByKeys(parentId, scaffoldKeys) { + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetEmptyByKeys'), { + contentTypeKeys: scaffoldKeys, + parentId: parentId + }), 'Failed to retrieve data for empty content items ids' + scaffoldKeys.join(', ')).then(function (result) { + Object.keys(result).map(function (key) { + result[key] = umbDataFormatter.formatContentGetData(result[key]); + }); + return $q.when(result); + }); + }, getBlueprintScaffold: function getBlueprintScaffold(parentId, blueprintId) { - return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetEmpty', [ - { blueprintId: blueprintId }, - { parentId: parentId } - ])), 'Failed to retrieve blueprint for id ' + blueprintId).then(function (result) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'GetEmpty', { + blueprintId: blueprintId, + parentId: parentId + })), 'Failed to retrieve blueprint for id ' + blueprintId).then(function (result) { return $q.when(umbDataFormatter.formatContentGetData(result)); }); }, @@ -1034,7 +1292,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct @@ -1045,10 +1303,10 @@ } //converts the value to a js bool function toBool(v) { - if (angular.isNumber(v)) { + if (Utilities.isNumber(v)) { return v > 0; } - if (angular.isString(v)) { + if (Utilities.isString(v)) { return v === 'true'; } if (typeof v === 'boolean') { @@ -1077,7 +1335,7 @@ * @methodOf umbraco.resources.contentResource * * @description - * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation + * Saves changes made to a content item to its current version, if the content item is new, the isNew parameter must be passed to force creation * if the content item needs to have files attached, they must be provided as the files param and passed separately * * @@ -1104,6 +1362,34 @@ var endpoint = umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSave'); return saveContentItem(content, 'save' + (isNew ? 'New' : ''), files, endpoint, showNotifications); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#saveBlueprint + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content blueprint item to its current version, if the content blueprint item is new, the isNew parameter must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * ##usage + *
      +    * contentResource.getById(1234)
      +    *    .then(function(content) {
      +    *          content.name = "I want a new name!";
      +    *          contentResource.saveBlueprint(content, false)
      +    *            .then(function(content){
      +    *                alert("Retrieved, updated and saved again");
      +    *            });
      +    *    });
      +    * 
      + * + * @param {Object} content The content blueprint item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @param {Bool} showNotifications an option to disable/show notifications (default is true) + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ saveBlueprint: function saveBlueprint(content, isNew, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSaveBlueprint'); return saveContentItem(content, 'save' + (isNew ? 'New' : ''), files, endpoint, showNotifications); @@ -1114,7 +1400,7 @@ * @methodOf umbraco.resources.contentResource * * @description - * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation + * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew parameter must be passed to force creation * if the content item needs to have files attached, they must be provided as the files param and passed separately * * @@ -1141,6 +1427,35 @@ var endpoint = umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSave'); return saveContentItem(content, 'publish' + (isNew ? 'New' : ''), files, endpoint, showNotifications); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves and publishes changes made to a content item and its descendants to a new version, if the content item is new, the isNew parameter must be passed to force creation + * if the content items needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
      +    * contentResource.getById(1234)
      +    *    .then(function(content) {
      +    *          content.name = "I want a new name, and be published!";
      +    *          contentResource.publishWithDescendants(content, false)
      +    *            .then(function(content){
      +    *                alert("Retrieved, updated and published again");
      +    *            });
      +    *    });
      +    * 
      + * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @param {Bool} showNotifications an option to disable/show notifications (default is true) + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ publishWithDescendants: function publishWithDescendants(content, isNew, force, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostSave'); var action = 'publishWithDescendants'; @@ -1223,6 +1538,27 @@ } return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'PostPublishById', [{ id: id }])), 'Failed to publish content with id ' + id); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#createBlueprintFromContent + * @methodOf umbraco.resources.contentResource + * + * @description + * Creates a content blueprint with a given name from a given content id + * + * ##usage + *
      +    * contentResource.createBlueprintFromContent(1234,"name")
      +    *    .then(function(content) {
      +    *        alert("created");
      +    *    });
      +        * 
      + * + * @param {Int} id The ID of the content to create the content blueprint from + * @param {string} id The name of the content blueprint + * @returns {Promise} resourcePromise object + * + */ createBlueprintFromContent: function createBlueprintFromContent(contentId, name) { return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentApiBaseUrl', 'CreateBlueprintFromContent', { contentId: contentId, @@ -1365,9 +1701,9 @@ loginPageId: loginPageId, errorPageId: errorPageId }; - if (angular.isArray(groups) && groups.length) { + if (Utilities.isArray(groups) && groups.length) { publicAccess.groups = groups; - } else if (angular.isArray(usernames) && usernames.length) { + } else if (Utilities.isArray(usernames) && usernames.length) { publicAccess.usernames = usernames; } else { throw 'must supply either userName/password or roles'; @@ -1408,9 +1744,51 @@ **/ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, localizationService, notificationsService) { return { + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#getCount + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Gets the count of content types + * + * ##usage + *
      +    * contentTypeResource.getCount()
      +    *    .then(function(data) {
      +    *        console.log(data);
      +    *    });
      +    * 
      + * + * @returns {Promise} resourcePromise object. + * + */ getCount: function getCount() { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetCount')), 'Failed to retrieve count'); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#getAvailableCompositeContentTypes + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Gets the compositions for a content type + * + * ##usage + *
      +    * contentTypeResource.getAvailableCompositeContentTypes()
      +    *    .then(function(data) {
      +    *        console.log(data);
      +    *    });
      +    * 
      + * + * @param {Int} contentTypeId id of the content type to retrieve the list of the compositions + * @param {Array} filterContentTypes array of content types to filter out + * @param {Array} filterPropertyTypes array of property aliases to filter out. If specified any content types with the property aliases will be filtered out + * @param {Boolean} isElement whether the composite content types should be applicable for an element type + * @returns {Promise} resourcePromise object. + * + */ getAvailableCompositeContentTypes: function getAvailableCompositeContentTypes(contentTypeId, filterContentTypes, filterPropertyTypes, isElement) { if (!filterContentTypes) { filterContentTypes = []; @@ -1464,6 +1842,7 @@ * $scope.type = type; * }); * + * * @param {Int} contentTypeId id of the content item to retrive allowed child types for * @returns {Promise} resourcePromise object. * @@ -1479,41 +1858,168 @@ * @description * Returns a list of defined property type aliases * + * ##usage + *
      +     * contentTypeResource.getAllPropertyTypeAliases()
      +     *    .then(function(array) {
      +     *       Do stuff...
      +     *    });
      +     * 
      + * * @returns {Promise} resourcePromise object. * */ getAllPropertyTypeAliases: function getAllPropertyTypeAliases() { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetAllPropertyTypeAliases')), 'Failed to retrieve property type aliases'); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#getAllStandardFields + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Returns a list of standard property type aliases + * + * ##usage + *
      +    * contentTypeResource.getAllStandardFields()
      +    *    .then(function(array) {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @returns {Promise} resourcePromise object. + * + */ getAllStandardFields: function getAllStandardFields() { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetAllStandardFields')), 'Failed to retrieve standard fields'); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#getPropertyTypeScaffold + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Returns the property display for a given datatype id + * + * ##usage + *
      +    * contentTypeResource.getPropertyTypeScaffold(1234)
      +    *    .then(function(array) {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @param {Int} id the id of the datatype + * @returns {Promise} resourcePromise object. + * + */ getPropertyTypeScaffold: function getPropertyTypeScaffold(id) { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetPropertyTypeScaffold', [{ id: id }])), 'Failed to retrieve property type scaffold'); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#getById + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Get the content type with a given id + * + * ##usage + *
      +    * contentTypeResource.getById("64058D0F-4911-4AB7-B3BA-000D89F00A26")
      +    *    .then(function(array) {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @param {String} id the guid id of the content type + * @returns {Promise} resourcePromise object. + * + */ getById: function getById(id) { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetById', [{ id: id }])), 'Failed to retrieve content type'); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#deleteById + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Delete the content type of a given id + * + * ##usage + *
      +    * contentTypeResource.deleteById(1234)
      +    *    .then(function(array) {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @param {Int} id the id of the content type + * @returns {Promise} resourcePromise object. + * + */ deleteById: function deleteById(id) { return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'DeleteById', [{ id: id }])), 'Failed to delete content type'); }, - deleteContainerById: function deleteContainerById(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 - * - * @description - * Returns a list of all content types - * - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.contentTypeResource#deleteContainerById + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Delete the content type container of a given id + * + * ##usage + *
      +    * contentTypeResource.deleteContainerById(1234)
      +    *    .then(function(array) {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @param {Int} id the id of the content type container + * @returns {Promise} resourcePromise object. + * + */ + deleteContainerById: function deleteContainerById(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 + * + * @description + * Returns a list of all content types + * + * @returns {Promise} resourcePromise object. + * + */ getAll: function getAll() { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetAll')), 'Failed to retrieve all content types'); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#getScaffold + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Returns an empty content type for use as a scaffold when creating a new content type + * + * ##usage + *
      +    * contentTypeResource.getScaffold(1234)
      +    *    .then(function(array) {
      +    *       Do stuff...
      +    *    });
      +    * 
      + * + * @param {Int} id the parent id + * @returns {Promise} resourcePromise object. + * + */ getScaffold: function getScaffold(parentId) { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'GetEmpty', { parentId: parentId })), 'Failed to retrieve content type scaffold'); }, @@ -1545,14 +2051,14 @@ *
            * contentTypeResource.move({ parentId: 1244, id: 123 })
            *    .then(function() {
      -     *        alert("node was moved");
      +     *        alert("content type was moved");
            *    }, function(err){
      -     *      alert("node didnt move:" + err.data.Message);
      +     *      alert("content type didnt move:" + err.data.Message);
            *    });
            * 
      * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to + * @param {Int} args.id the ID of the content type to move + * @param {Int} args.parentId the ID of the parent content type to move to * @returns {Promise} resourcePromise object. * */ @@ -1566,11 +2072,35 @@ if (!args.id) { throw 'args.id cannot be null'; } + var promise = localizationService.localize('contentType_moveFailed'); return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostMove'), { parentId: args.parentId, id: args.id - }, { responseType: 'text' }), 'Failed to move content'); + }, { responseType: 'text' }), promise); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#copy + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Copied a content type underneath a new parentId + * + * ##usage + *
      +     * contentTypeResource.copy({ parentId: 1244, id: 123 })
      +     *    .then(function() {
      +     *        alert("content type was copied");
      +     *    }, function(err){
      +     *      alert("content type didnt copy:" + err.data.Message);
      +     *    });
      +     * 
      + * @param {Object} args arguments object + * @param {Int} args.id the ID of the content type to copy + * @param {Int} args.parentId the ID of the parent content type to copy to + * @returns {Promise} resourcePromise object. + * + */ copy: function copy(args) { if (!args) { throw 'args cannot be null'; @@ -1581,34 +2111,87 @@ if (!args.id) { throw 'args.id cannot be null'; } + var promise = localizationService.localize('contentType_copyFailed'); return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostCopy'), { parentId: args.parentId, id: args.id - }, { responseType: 'text' }), 'Failed to copy content'); + }, { responseType: 'text' }), promise); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#createContainer + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Create a new content type container of a given name underneath a given parent item + * + * ##usage + *
      +    * contentTypeResource.createContainer(1244,"testcontainer")
      +    *    .then(function() {
      +    *       Do stuff..
      +    *    });
      +    * 
      + * + * @param {Int} parentId the ID of the parent content type underneath which to create the container + * @param {String} name the name of the container + * @returns {Promise} resourcePromise object. + * + */ createContainer: function createContainer(parentId, name) { return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostCreateContainer', { parentId: parentId, name: encodeURIComponent(name) })), 'Failed to create a folder under parent id ' + parentId); }, - createCollection: function createCollection(parentId, collectionName, collectionCreateTemplate, collectionItemName, collectionItemCreateTemplate, collectionIcon, collectionItemIcon) { - return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostCreateCollection', { - parentId: parentId, - collectionName: collectionName, - collectionCreateTemplate: collectionCreateTemplate, - collectionItemName: collectionItemName, - collectionItemCreateTemplate: collectionItemCreateTemplate, - collectionIcon: collectionIcon, - collectionItemIcon: collectionItemIcon - })), 'Failed to create collection under ' + parentId); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#renameContainer + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Rename a container of a given id + * + * ##usage + *
      +    * contentTypeResource.renameContainer( 1244,"testcontainer")
      +    *    .then(function() {
      +    *       Do stuff..
      +    *    });
      +    * 
      + * + * @param {Int} id the ID of the container to rename + * @param {String} name the new name of the container + * @returns {Promise} resourcePromise object. + * + */ renameContainer: function renameContainer(id, name) { return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostRenameContainer', { id: id, name: name })), 'Failed to rename the folder with id ' + id); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#export + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Export a content type of a given id. + * + * ##usage + *
      +    * contentTypeResource.export(1234){
      +    *    .then(function() {
      +    *       Do stuff..
      +    *    });
      +    * 
      + * + * @param {Int} id the ID of the container to rename + * @param {String} name the new name of the container + * @returns {Promise} resourcePromise object. + * + */ export: function _export(id) { if (!id) { throw 'id cannot be null'; @@ -1624,15 +2207,75 @@ }); }); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#import + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Import a content type from a file + * + * ##usage + *
      +    * contentTypeResource.import("path to file"){
      +    *    .then(function() {
      +    *       Do stuff..
      +    *    });
      +    * 
      + * + * @param {String} file path of the file to import + * @returns {Promise} resourcePromise object. + * + */ import: function _import(file) { if (!file) { throw 'file cannot be null'; } return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'Import', { file: file })), 'Failed to import document type ' + file); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#createDefaultTemplate + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Create a default template for a content type with a given id + * + * ##usage + *
      +    * contentTypeResource.createDefaultTemplate(1234){
      +    *    .then(function() {
      +    *       Do stuff..
      +    *    });
      +    * 
      + * + * @param {Int} id the id of the content type for which to create the default template + * @returns {Promise} resourcePromise object. + * + */ createDefaultTemplate: function createDefaultTemplate(id) { return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'PostCreateDefaultTemplate', { id: id })), 'Failed to create default template for content type with id ' + id); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#hasContentNodes + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Returns whether a content type has content nodes + * + * ##usage + *
      +    * contentTypeResource.hasContentNodes(1234){
      +    *    .then(function() {
      +    *       Do stuff..
      +    *    });
      +    * 
      + * + * @param {Int} id the id of the content type + * @returns {Promise} resourcePromise object. + * + */ hasContentNodes: function hasContentNodes(id) { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('contentTypeApiBaseUrl', 'HasContentNodes', [{ id: id }])), 'Failed to retrieve indication for whether content type with id ' + id + ' has associated content nodes'); } @@ -1694,7 +2337,7 @@ if (!newPassword) { return $q.reject({ errorMsg: 'newPassword cannot be empty' }); } - return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('currentUserApiBaseUrl', 'PostSetInvitedUserPassword'), angular.toJson(newPassword)), 'Failed to change password'); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('currentUserApiBaseUrl', 'PostSetInvitedUserPassword'), Utilities.toJson(newPassword)), 'Failed to change password'); }, /** * @ngdoc method @@ -2192,6 +2835,39 @@ angular.module('umbraco.resources').factory('dictionaryResource', dictionaryResource); 'use strict'; /** + * @ngdoc service + * @name umbraco.resources.elementTypeResource + * @description Loads in data for element types + **/ + function elementTypeResource($q, $http, umbRequestHelper) { + return { + /** + * @ngdoc method + * @name umbraco.resources.elementTypeResource#getAll + * @methodOf umbraco.resources.elementTypeResource + * + * @description + * Gets a list of all element types + * + * ##usage + *
      +    * elementTypeResource.getAll()
      +    *    .then(function() {
      +    *        alert('Found it!');
      +    *    });
      +    * 
      + * + * @returns {Promise} resourcePromise object. + * + **/ + getAll: function getAll() { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('elementTypeApiBaseUrl', 'GetAll')), 'Failed to retrieve element types'); + } + }; + } + angular.module('umbraco.resources').factory('elementTypeResource', elementTypeResource); + 'use strict'; + /** * @ngdoc service * @name umbraco.resources.emailMarketingResource * @description Used to add a backoffice user to Umbraco's email marketing system, if user opts in @@ -2386,7 +3062,7 @@ }, getAnchors: function getAnchors(rteContent) { if (!rteContent || rteContent.length === 0) { - return []; + return $q.when([]); } return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('entityApiBaseUrl', 'GetAnchors'), { rteContent: rteContent }), 'Failed to anchors data for rte content ' + rteContent); }, @@ -2569,7 +3245,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct @@ -2630,7 +3306,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct @@ -2778,6 +3454,32 @@ angular.module('umbraco.resources').factory('healthCheckResource', healthCheckResource); }()); 'use strict'; + /** + * @ngdoc service + * @name umbraco.resources.imageUrlGeneratorResource + * @function + * + * @description + * Used by the various controllers to get an image URL formatted correctly for the current image URL generator + */ + (function () { + 'use strict'; + function imageUrlGeneratorResource($http, umbRequestHelper) { + function getCropUrl(mediaPath, width, height, imageCropMode, animationProcessMode) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('imageUrlGeneratorApiBaseUrl', 'GetCropUrl', { + mediaPath: mediaPath, + width: width, + height: height, + imageCropMode: imageCropMode, + animationProcessMode: animationProcessMode + })), 'Failed to get crop URL'); + } + var resource = { getCropUrl: getCropUrl }; + return resource; + } + angular.module('umbraco.resources').factory('imageUrlGeneratorResource', imageUrlGeneratorResource); + }()); + 'use strict'; (function () { 'use strict'; /** @@ -2901,7 +3603,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; if (options.hasOwnProperty('sinceDate')) { @@ -2958,7 +3660,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; if (options.hasOwnProperty('sinceDate')) { @@ -3112,7 +3814,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('logViewerApiBaseUrl', 'GetLogs', options)), 'Failed to retrieve common log messages'); @@ -3392,8 +4094,8 @@ */ getByIds: function getByIds(ids) { var idQuery = ''; - _.each(ids, function (item) { - idQuery += 'ids=' + item + '&'; + ids.forEach(function (id) { + return idQuery += 'ids='.concat(id, '&'); }); return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'GetByIds', idQuery)), 'Failed to retrieve data for media ids ' + ids); }, @@ -3478,7 +4180,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct @@ -3489,10 +4191,10 @@ } //converts the value to a js bool function toBool(v) { - if (angular.isNumber(v)) { + if (Utilities.isNumber(v)) { return v > 0; } - if (angular.isString(v)) { + if (Utilities.isString(v)) { return v === 'true'; } if (typeof v === 'boolean') { @@ -3668,7 +4370,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'GetPagedReferences', { @@ -3815,7 +4517,7 @@ if (!args.id) { throw 'args.id cannot be null'; } - var promise = localizationService.localize('media_moveFailed'); + var promise = localizationService.localize('mediaType_moveFailed'); return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'PostMove'), { parentId: args.parentId, id: args.id @@ -3831,7 +4533,7 @@ if (!args.id) { throw 'args.id cannot be null'; } - var promise = localizationService.localize('media_copyFailed'); + var promise = localizationService.localize('mediaType_copyFailed'); return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('mediaTypeApiBaseUrl', 'PostCopy'), { parentId: args.parentId, id: args.id @@ -3890,7 +4592,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct @@ -3901,10 +4603,10 @@ } //converts the value to a js bool function toBool(v) { - if (angular.isNumber(v)) { + if (Utilities.isNumber(v)) { return v > 0; } - if (angular.isString(v)) { + if (Utilities.isString(v)) { return v === 'true'; } if (typeof v === 'boolean') { @@ -4063,8 +4765,8 @@ }, getByIds: function getByIds(ids) { var idQuery = ''; - _.each(ids, function (item) { - idQuery += 'ids=' + item + '&'; + ids.forEach(function (id) { + return idQuery += 'ids='.concat(id, '&'); }); return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('memberGroupApiBaseUrl', 'GetByIds', idQuery)), 'Failed to retrieve member group'); }, @@ -4098,7 +4800,7 @@ * @name umbraco.resources.memberTypeResource * @description Loads in data for member types **/ - function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { + function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter, localizationService) { return { getAvailableCompositeContentTypes: function getAvailableCompositeContentTypes(contentTypeId, filterContentTypes, filterPropertyTypes) { if (!filterContentTypes) { @@ -4108,15 +4810,15 @@ filterPropertyTypes = []; } var query = ''; - _.each(filterContentTypes, function (item) { - query += 'filterContentTypes=' + item + '&'; + filterContentTypes.forEach(function (fct) { + return query += 'filterContentTypes='.concat(fct, '&'); }); // 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 + '&'; + filterPropertyTypes.forEach(function (fpt) { + return query += 'filterPropertyTypes='.concat(fpt, '&'); }); // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error if (filterPropertyTypes.length === 0) { @@ -4153,19 +4855,97 @@ save: function save(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); + }, + copy: function copy(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'; + } + var promise = localizationService.localize('memberType_copyFailed'); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('memberTypeApiBaseUrl', 'PostCopy'), { + parentId: args.parentId, + id: args.id + }, { responseType: 'text' }), promise); } }; } angular.module('umbraco.resources').factory('memberTypeResource', memberTypeResource); 'use strict'; + /** +* @ngdoc service +* @name umbraco.resources.modelsBuilderManagementResource +* @description Resources to get information on modelsbuilder status and build models +**/ function modelsBuilderManagementResource($q, $http, umbRequestHelper) { return { + /** + * @ngdoc method + * @name umbraco.resources.modelsBuilderManagementResource#getModelsOutOfDateStatus + * @methodOf umbraco.resources.modelsBuilderManagementResource + * + * @description + * Gets the status of modelsbuilder + * + * ##usage + *
      +    * modelsBuilderManagementResource.getModelsOutOfDateStatus()
      +    *  .then(function() {
      +    *        Do stuff...*
      +    * });
      +    * 
      + * + * @returns {Promise} resourcePromise object. + * + */ getModelsOutOfDateStatus: function getModelsOutOfDateStatus() { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('modelsBuilderBaseUrl', 'GetModelsOutOfDateStatus')), 'Failed to get models out-of-date status'); }, + /** + * @ngdoc method + * @name umbraco.resources.modelsBuilderManagementResource#buildModels + * @methodOf umbraco.resources.modelsBuilderManagementResource + * + * @description + * Builds the models + * + * ##usage + *
      +    * modelsBuilderManagementResource.buildModels()
      +    *  .then(function() {
      +    *        Do stuff...*
      +    * });
      +    * 
      + * + * @returns {Promise} resourcePromise object. + * + */ buildModels: function buildModels() { return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('modelsBuilderBaseUrl', 'BuildModels')), 'Failed to build models'); }, + /** + * @ngdoc method + * @name umbraco.resources.modelsBuilderManagementResource#getDashboard + * @methodOf umbraco.resources.modelsBuilderManagementResource + * + * @description + * Gets the modelsbuilder dashboard + * + * ##usage + *
      +    * modelsBuilderManagementResource.getDashboard()
      +    *  .then(function() {
      +    *        Do stuff...*
      +    * });
      +    * 
      + * + * @returns {Promise} resourcePromise object. + * + */ getDashboard: function getDashboard() { return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('modelsBuilderBaseUrl', 'GetDashboard')), 'Failed to get dashboard'); } @@ -4633,7 +5413,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('relationTypeApiBaseUrl', 'GetPagedResults', { @@ -5324,7 +6104,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct @@ -5377,6 +6157,29 @@ } /** * @ngdoc method + * @name umbraco.resources.usersResource#getUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Gets users from ids + * + * ##usage + *
      +      * usersResource.getUsers([1,2,3])
      +      *    .then(function(data) {
      +      *        alert("It's here");
      +      *    });
      +      * 
      + * + * @param {Array} userIds user ids. + * @returns {Promise} resourcePromise object containing the users array. + * + */ + function getUsers(userIds) { + return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('userApiBaseUrl', 'GetByIds', { ids: userIds })), 'Failed to retrieve data for users ' + userIds); + } + /** + * @ngdoc method * @name umbraco.resources.usersResource#createUser * @methodOf umbraco.resources.usersResource * @@ -5461,6 +6264,34 @@ } /** * @ngdoc method + * @name umbraco.resources.usersResource#changePassword + * @methodOf umbraco.resources.usersResource + * + * @description + * Changes a user's password + * + * ##usage + *
      +      * usersResource.changePassword(changePasswordModel)
      +      *    .then(function() {
      +      *        // password changed
      +      *    });
      +      * 
      + * + * @param {Object} model object to save + * @returns {Promise} resourcePromise object containing the updated user. + * + */ + function changePassword(changePasswordModel) { + if (!changePasswordModel) { + throw 'password model not specified'; + } + //need to convert the password data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedPasswordData = umbDataFormatter.formatChangePasswordModel(changePasswordModel); + return umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('userApiBaseUrl', 'PostChangePassword'), formattedPasswordData), 'Failed to save user'); + } + /** + * @ngdoc method * @name umbraco.resources.usersResource#deleteNonLoggedInUser * @methodOf umbraco.resources.usersResource * @@ -5489,9 +6320,11 @@ setUserGroupsOnUsers: setUserGroupsOnUsers, getPagedResults: getPagedResults, getUser: getUser, + getUsers: getUsers, createUser: createUser, inviteUser: inviteUser, saveUser: saveUser, + changePassword: changePassword, deleteNonLoggedInUser: deleteNonLoggedInUser, clearAvatar: clearAvatar }; diff --git a/TestSite/Umbraco/Js/umbraco.services.js b/TestSite/Umbraco/Js/umbraco.services.js index 765c844..cb0c3cb 100644 --- a/TestSite/Umbraco/Js/umbraco.services.js +++ b/TestSite/Umbraco/Js/umbraco.services.js @@ -14,29 +14,110 @@ * Some angular helper/extension methods */ function angularHelper($q) { + var requiredFormProps = [ + '$error', + '$name', + '$dirty', + '$pristine', + '$valid', + '$submitted', + '$pending' + ]; + function collectAllFormErrorsRecursively(formCtrl, allErrors) { + // skip if the control is already added to the array + if (allErrors.indexOf(formCtrl) !== -1) { + return; + } + // loop over the error dictionary (see https://docs.angularjs.org/api/ng/type/form.FormController#$error) + var keys = Object.keys(formCtrl.$error); + if (keys.length === 0) { + return; + } + keys.forEach(function (validationKey) { + var ctrls = formCtrl.$error[validationKey]; + ctrls.forEach(function (ctrl) { + if (!ctrl) { + // this happens when $setValidity('err', true) is called on a form controller without specifying the 3rd parameter for the control/form + // which just means that this is an error on the formCtrl itself + allErrors.push(formCtrl); // add the error + } else if (isForm(ctrl)) { + // sometimes the control in error is the same form so we cannot recurse else we'll cause an infinite loop + // and in this case it means the error is assigned directly to the form, not a control + if (ctrl === formCtrl) { + allErrors.push(ctrl); + // add the error + return; + } + // recurse with the sub form + collectAllFormErrorsRecursively(ctrl, allErrors); + } else { + // it's a normal control + allErrors.push(ctrl); // add the error + } + }); + }); + } + function isForm(obj) { + // a method to check that the collection of object prop names contains the property name expected + function allPropertiesExist(objectPropNames) { + //ensure that every required property name exists on the current object + return _.every(requiredFormProps, function (item) { + return _.contains(objectPropNames, item); + }); + } + //get the keys of the property names for the current object + var props = _.keys(obj); + //if the length isn't correct, try the next prop + if (props.length < requiredFormProps.length) { + return false; + } + //ensure that every required property name exists on the current scope property + return allPropertiesExist(props); + } return { + countAllFormErrors: function countAllFormErrors(formCtrl) { + var allErrors = []; + collectAllFormErrorsRecursively(formCtrl, allErrors); + return allErrors.length; + }, + /** + * Will traverse up the $scope chain to all ancestors until the predicate matches for the current scope or until it's at the root. + * @param {any} scope + * @param {any} predicate + */ + traverseScopeChain: function traverseScopeChain(scope, predicate) { + var s = scope.$parent; + while (s) { + var result = predicate(s); + if (result === true) { + return s; + } + s = s.$parent; + } + return null; + }, /** * Method used to re-run the $parsers for a given ngModel - * @param {} scope - * @param {} ngModel - * @returns {} + * @param {} scope + * @param {} ngModel + * @returns {} */ revalidateNgModel: function revalidateNgModel(scope, ngModel) { this.safeApply(scope, function () { - angular.forEach(ngModel.$parsers, function (parser) { + ngModel.$parsers.forEach(function (parser) { parser(ngModel.$viewValue); }); }); }, /** * Execute a list of promises sequentially. Unlike $q.all which executes all promises at once, this will execute them in sequence. - * @param {} promises - * @returns {} + * @param {} promises + * @returns {} */ executeSequentialPromises: function executeSequentialPromises(promises) { //this is sequential promise chaining, it's not pretty but we need to do it this way. //$q.all doesn't execute promises in sequence but that's what we want to do here. - if (!angular.isArray(promises)) { + if (!Utilities.isArray(promises)) { throw 'promises must be an array'; } //now execute them in sequence... sorry there's no other good way to do it with angular promises @@ -68,17 +149,18 @@ */ safeApply: function safeApply(scope, fn) { if (scope.$$phase || scope.$root && scope.$root.$$phase) { - if (angular.isFunction(fn)) { + if (Utilities.isFunction(fn)) { fn(); } } else { - if (angular.isFunction(fn)) { + if (Utilities.isFunction(fn)) { scope.$apply(fn); } else { scope.$apply(); } } }, + isForm: isForm, /** * @ngdoc function * @name getCurrentForm @@ -92,39 +174,15 @@ //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 + // 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', - '$submitted', - '$pending' - ]; - // 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) { + if (this.isForm(scope[p])) { form = scope[p]; break; } @@ -171,11 +229,11 @@ $valid: true, $submitted: false, $pending: undefined, - $addControl: angular.noop, - $removeControl: angular.noop, - $setValidity: angular.noop, - $setDirty: angular.noop, - $setPristine: angular.noop, + $addControl: Utilities.noop, + $removeControl: Utilities.noop, + $setValidity: Utilities.noop, + $setDirty: Utilities.noop, + $setPristine: Utilities.noop, $name: formName }; } @@ -688,7 +746,7 @@ */ load: function load(pathArray, scope, defaultAssetType) { var promise; - if (!angular.isArray(pathArray)) { + if (!Utilities.isArray(pathArray)) { throw 'pathArray must be an array'; } // Check to see if there's anything to load, resolve promise if not @@ -705,7 +763,7 @@ //blocking var promises = []; var assets = []; - _.each(nonEmpty, function (path) { + nonEmpty.forEach(function (path) { path = convertVirtualPath(path); var asset = service._getAssetPromise(path); //if not previously loaded, add to list of promises @@ -749,23 +807,23 @@ scope = $rootScope; } angularHelper.safeApply(scope, function () { - asset.deferred.resolve(true); + return asset.deferred.resolve(true); }); } if (cssAssets.length > 0) { - var cssPaths = _.map(cssAssets, function (asset) { - return appendRnd(asset.path); + var cssPaths = cssAssets.map(function (css) { + return appendRnd(css.path); }); LazyLoad.css(cssPaths, function () { - _.each(cssAssets, assetLoaded); + return cssAssets.forEach(assetLoaded); }); } if (jsAssets.length > 0) { - var jsPaths = _.map(jsAssets, function (asset) { - return appendRnd(asset.path); + var jsPaths = jsAssets.map(function (js) { + return appendRnd(js.path); }); LazyLoad.js(jsPaths, function () { - _.each(jsAssets, assetLoaded); + return jsAssets.forEach(assetLoaded); }); } return promise; @@ -825,7 +883,11 @@ * */ function close() { - args.opacity = null, args.element = null, args.elementPreventClick = false, args.disableEventsOnClick = false, args.show = false; + args.opacity = null; + args.element = null; + args.elementPreventClick = false; + args.disableEventsOnClick = false; + args.show = false; eventsService.emit('appState.backdrop', args); } /** @@ -864,140 +926,1244 @@ angular.module('umbraco.services').factory('backdropService', backdropService); }()); 'use strict'; + function _typeof(obj) { + if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') { + _typeof = function _typeof(obj) { + return typeof obj; + }; + } else { + _typeof = function _typeof(obj) { + return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj; + }; + } + return _typeof(obj); + } /** * @ngdoc service - * @name umbraco.services.clipboardService + * @name umbraco.services.blockEditorService * - * @requires notificationsService - * @requires eventsService + * @description + * Added in Umbraco 8.7. Service for dealing with Block Editors. + * + * Block Editor Service provides the basic features for a block editor. + * The main feature is the ability to create a Model Object which takes care of your data for your Block Editor. + * + * + * ##Samples + * + * ####Instantiate a Model Object for your property editor: + * + *
      + *     modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope);
      + *     modelObject.load().then(onLoaded);
      + * 
      + * + * + * See {@link umbraco.services.blockEditorModelObject BlockEditorModelObject} for more samples. + * + */ + (function () { + 'use strict'; + /** + * When performing a runtime copy of Block Editors entries, we copy the ElementType Data Model and inner IDs are kept identical, to ensure new IDs are changed on paste we need to provide a resolver for the ClipboardService. + */ + angular.module('umbraco').run([ + 'clipboardService', + 'udiService', + function (clipboardService, udiService) { + function replaceUdi(obj, key, dataObject) { + var udi = obj[key]; + var newUdi = udiService.create('element'); + obj[key] = newUdi; + dataObject.forEach(function (data) { + if (data.udi === udi) { + data.udi = newUdi; + } + }); + } + function replaceUdisOfObject(obj, propValue) { + for (var k in obj) { + if (k === 'contentUdi') { + replaceUdi(obj, k, propValue.contentData); + } else if (k === 'settingsUdi') { + replaceUdi(obj, k, propValue.settingsData); + } else { + // lets crawl through all properties of layout to make sure get captured all `contentUdi` and `settingsUdi` properties. + var propType = _typeof(obj[k]); + if (propType != null && (propType === 'object' || propType === 'array')) { + replaceUdisOfObject(obj[k], propValue); + } + } + } + } + function removeBlockReferences(obj) { + for (var k in obj) { + if (k === 'contentUdi') { + delete obj[k]; + } else if (k === 'settingsUdi') { + delete obj[k]; + } else { + // lets crawl through all properties of layout to make sure get captured all `contentUdi` and `settingsUdi` properties. + var propType = _typeof(obj[k]); + if (propType != null && (propType === 'object' || propType === 'array')) { + removeBlockReferences(obj[k]); + } + } + } + } + function elementTypeBlockResolver(obj, propPasteResolverMethod) { + // we could filter for specific Property Editor Aliases, but as the Block Editor structure can be used by many Property Editor we do not in this code know a good way to detect that this is a Block Editor and will therefor leave it to the value structure to determin this. + rawBlockResolver(obj.value, propPasteResolverMethod); + } + clipboardService.registerPastePropertyResolver(elementTypeBlockResolver, clipboardService.TYPES.ELEMENT_TYPE); + function rawBlockResolver(value, propPasteResolverMethod) { + if (value != null && _typeof(value) === 'object') { + // we got an object, and it has these three props then we are most likely dealing with a Block Editor. + if (value.layout !== undefined && value.contentData !== undefined && value.settingsData !== undefined) { + replaceUdisOfObject(value.layout, value); + // run resolvers for inner properties of this Blocks content data. + if (value.contentData.length > 0) { + value.contentData.forEach(function (item) { + for (var k in item) { + propPasteResolverMethod(item[k], clipboardService.TYPES.RAW); + } + }); + } + // run resolvers for inner properties of this Blocks settings data. + if (value.settingsData.length > 0) { + value.settingsData.forEach(function (item) { + for (var k in item) { + propPasteResolverMethod(item[k], clipboardService.TYPES.RAW); + } + }); + } + } + } + } + clipboardService.registerPastePropertyResolver(rawBlockResolver, clipboardService.TYPES.RAW); + function provideNewUdisForBlockResolver(block, propPasteResolverMethod) { + if (block.layout) { + // We do not support layout child blocks currently, these should be stripped out as we only will be copying a single entry. + removeBlockReferences(block.layout); + } + if (block.data) { + // Make new UDI for content-element + block.data.udi = block.layout.contentUdi = udiService.create('element'); + } + if (block.settingsData) { + // Make new UDI for settings-element + block.settingsData.udi = block.layout.settingsUdi = udiService.create('element'); + } + } + clipboardService.registerPastePropertyResolver(provideNewUdisForBlockResolver, clipboardService.TYPES.BLOCK); + } + ]); + function blockEditorService(blockEditorModelObject) { + /** + * @ngdocs function + * @name createModelObject + * @methodOf umbraco.services.blockEditorService + * + * @description + * Create a new Block Editor Model Object. + * See {@link umbraco.services.blockEditorModelObject blockEditorModelObject} + * + * @see umbraco.services.blockEditorModelObject + * @param {object} propertyModelValue data object of the property editor, usually model.value. + * @param {string} propertyEditorAlias alias of the property. + * @param {object} blockConfigurations block configurations. + * @param {angular-scope} scopeOfExistance A local angularJS scope that exists as long as the data exists. + * @param {angular-scope} propertyEditorScope A local angularJS scope that represents the property editors scope. + * @return {blockEditorModelObject} A instance of the BlockEditorModelObject class. + */ + function createModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope) { + return new blockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope); + } + return { createModelObject: createModelObject }; + } + angular.module('umbraco.services').service('blockEditorService', blockEditorService); + }()); + 'use strict'; + /** + * @ngdoc service + * @name umbraco.services.blockEditorModelObject * * @description - * Service to handle clipboard in general across the application. Responsible for handling the data both storing and retrive. - * The service has a set way for defining a data-set by a entryType and alias, which later will be used to retrive the posible entries for a paste scenario. + * Added in Umbraco 8.7. Model Object for dealing with data of Block Editors. + * + * Block Editor Model Object provides the basic features for editing data of a block editor.
      + * Use the Block Editor Service to instantiate the Model Object.
      + * See {@link umbraco.services.blockEditorService blockEditorService} * */ - function clipboardService(notificationsService, eventsService, localStorageService, iconHelper) { - var clearPropertyResolvers = []; - var STORAGE_KEY = 'umbClipboardService'; - var retriveStorage = function retriveStorage() { - if (localStorageService.isSupported === false) { - return null; + (function () { + 'use strict'; + function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService, notificationsService) { + /** + * Simple mapping from property model content entry to editing model, + * needs to stay simple to avoid deep watching. + */ + function mapToElementModel(elementModel, dataModel) { + if (!elementModel || !elementModel.variants || !elementModel.variants.length) { + return; + } + var variant = elementModel.variants[0]; + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (dataModel[prop.alias]) { + prop.value = dataModel[prop.alias]; + } + } + } } - var dataJSON; - var dataString = localStorageService.get(STORAGE_KEY); - if (dataString != null) { - dataJSON = JSON.parse(dataString); + /** + * Simple mapping from elementModel to property model content entry, + * needs to stay simple to avoid deep watching. + */ + function mapToPropertyModel(elementModel, dataModel) { + if (!elementModel || !elementModel.variants || !elementModel.variants.length) { + return; + } + var variant = elementModel.variants[0]; + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (prop.value) { + dataModel[prop.alias] = prop.value; + } + } + } } - if (dataJSON == null) { - dataJSON = new Object(); + /** + * Map property values from an ElementModel to another ElementModel. + * Used to tricker watchers for synchronization. + * @param {Object} fromModel ElementModel to recive property values from. + * @param {Object} toModel ElementModel to recive property values from. + */ + function mapElementValues(fromModel, toModel) { + if (!fromModel || !fromModel.variants) { + toModel.variants = null; + return; + } + if (!fromModel.variants.length) { + toModel.variants = []; + return; + } + var fromVariant = fromModel.variants[0]; + if (!fromVariant) { + toModel.variants = [null]; + return; + } + var toVariant = toModel.variants[0]; + for (var t = 0; t < fromVariant.tabs.length; t++) { + var fromTab = fromVariant.tabs[t]; + var toTab = toVariant.tabs.find(function (tab) { + return tab.alias === fromTab.alias; + }); + if (fromTab && fromTab.properties && fromTab.properties.length > 0 && toTab && toTab.properties && toTab.properties.length > 0) { + for (var p = 0; p < fromTab.properties.length; p++) { + var fromProp = fromTab.properties[p]; + var toProp = toTab.properties[p]; + toProp.value = fromProp.value; + } + } + } } - if (dataJSON.entries === undefined) { - dataJSON.entries = []; + /** + * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. + * @param {Object} blockObject BlockObject to recive data values from. + */ + function getBlockLabel(blockObject) { + if (blockObject.labelInterpolator !== undefined) { + var labelVars = Object.assign({ + '$settings': blockObject.settingsData || {}, + '$layout': blockObject.layout || {}, + '$index': (blockObject.index || 0) + 1 + }, blockObject.data); + return blockObject.labelInterpolator(labelVars); + } + return blockObject.content.contentTypeName; } - return dataJSON; - }; - var saveStorage = function saveStorage(storage) { - var storageString = JSON.stringify(storage); - try { - var storageJSON = JSON.parse(storageString); - localStorageService.set(STORAGE_KEY, storageString); - eventsService.emit('clipboardService.storageUpdate'); - return true; - } catch (e) { - return false; + /** + * Used to add watchers on all properties in a content or settings model + */ + function addWatchers(blockObject, isolatedScope, forSettings) { + var model = forSettings ? blockObject.settings : blockObject.content; + if (!model || !model.variants || !model.variants.length) { + return; + } + // Start watching each property value. + var variant = model.variants[0]; + var field = forSettings ? 'settings' : 'content'; + var watcherCreator = forSettings ? createSettingsModelPropWatcher : createContentModelPropWatcher; + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + // Watch value of property since this is the only value we want to keep synced. + // Do notice that it is not performing a deep watch, meaning that we are only watching primitive and changes directly to the object of property-value. + // But we like to sync non-primitive values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. + // Non-primitive values act as references to the same data and are therefor synced. + blockObject.__watchers.push(isolatedScope.$watch('blockObjects._' + blockObject.key + '.' + field + '.variants[0].tabs[' + t + '].properties[' + p + '].value', watcherCreator(blockObject, prop))); + // We also like to watch our data model to be able to capture changes coming from other places. + if (forSettings === true) { + blockObject.__watchers.push(isolatedScope.$watch('blockObjects._' + blockObject.key + '.' + 'settingsData' + '.' + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop))); + } else { + blockObject.__watchers.push(isolatedScope.$watch('blockObjects._' + blockObject.key + '.' + 'data' + '.' + prop.alias, createDataModelWatcher(blockObject, prop))); + } + } + } + if (blockObject.__watchers.length === 0) { + // If no watcher where created, it means we have no properties to watch. This means that nothing will activate our generate the label, since its only triggered by watchers. + blockObject.updateLabel(); + } } - return false; - }; - function clearPropertyForStorage(prop) { - for (var i = 0; i < clearPropertyResolvers.length; i++) { - clearPropertyResolvers[i](prop, clearPropertyForStorage); + /** + * Used to create a prop watcher for the data in the property editor data model. + */ + function createDataModelWatcher(blockObject, prop) { + return function () { + if (prop.value !== blockObject.data[prop.alias]) { + // sync data: + prop.value = blockObject.data[prop.alias]; + blockObject.updateLabel(); + } + }; } - } - var prepareEntryForStorage = function prepareEntryForStorage(entryData, firstLevelClearupMethod) { - var cloneData = angular.copy(entryData); - if (firstLevelClearupMethod != undefined) { - firstLevelClearupMethod(cloneData); + /** + * Used to create a prop watcher for the settings in the property editor data model. + */ + function createLayoutSettingsModelWatcher(blockObject, prop) { + return function () { + if (prop.value !== blockObject.settingsData[prop.alias]) { + // sync data: + prop.value = blockObject.settingsData[prop.alias]; + } + }; } - // remove keys from sub-entries - for (var t = 0; t < cloneData.variants[0].tabs.length; t++) { - var tab = cloneData.variants[0].tabs[t]; - for (var p = 0; p < tab.properties.length; p++) { - var prop = tab.properties[p]; - clearPropertyForStorage(prop); - } + /** + * Used to create a scoped watcher for a content property on a blockObject. + */ + function createContentModelPropWatcher(blockObject, prop) { + return function () { + if (blockObject.data[prop.alias] !== prop.value) { + // sync data: + blockObject.data[prop.alias] = prop.value; + } + blockObject.updateLabel(); + }; } - return cloneData; - }; - var isEntryCompatible = function isEntryCompatible(entry, type, allowedAliases) { - return entry.type === type && (entry.alias && allowedAliases.filter(function (allowedAlias) { - return allowedAlias === entry.alias; - }).length > 0 || entry.aliases && entry.aliases.filter(function (entryAlias) { - return allowedAliases.filter(function (allowedAlias) { - return allowedAlias === entryAlias; - }).length > 0; - }).length === entry.aliases.length); - }; - var service = {}; - /** - * @ngdoc method - * @name umbraco.services.clipboardService#registrerPropertyClearingResolver - * @methodOf umbraco.services.clipboardService - * - * @param {string} function A method executed for every property and inner properties copied. - * - * @description - * Executed for all properties including inner properties when performing a copy action. - */ - service.registrerClearPropertyResolver = function (resolver) { - clearPropertyResolvers.push(resolver); - }; - /** - * @ngdoc method - * @name umbraco.services.clipboardService#copy - * @methodOf umbraco.services.clipboardService - * - * @param {string} type A string defining the type of data to storing, example: 'elementType', 'contentNode' - * @param {string} alias A string defining the alias of the data to store, example: 'product' - * @param {object} entry A object containing the properties to be saved, this could be the object of a ElementType, ContentNode, ... - * @param {string} displayLabel (optional) A string swetting the label to display when showing paste entries. - * @param {string} displayIcon (optional) A string setting the icon to display when showing paste entries. - * @param {string} uniqueKey (optional) A string prodiving an identifier for this entry, existing entries with this key will be removed to ensure that you only have the latest copy of this data. - * - * @description - * Saves a single JS-object with a type and alias to the clipboard. - */ - service.copy = function (type, alias, data, displayLabel, displayIcon, uniqueKey, firstLevelClearupMethod) { - var storage = retriveStorage(); - displayLabel = displayLabel || data.name; - displayIcon = displayIcon || iconHelper.convertFromLegacyIcon(data.icon); - uniqueKey = uniqueKey || data.key || console.error('missing unique key for this content'); - // remove previous copies of this entry: - storage.entries = storage.entries.filter(function (entry) { - return entry.unique !== uniqueKey; - }); - var entry = { - unique: uniqueKey, - type: type, - alias: alias, - data: prepareEntryForStorage(data, firstLevelClearupMethod), - label: displayLabel, - icon: displayIcon - }; - storage.entries.push(entry); - if (saveStorage(storage) === true) { - notificationsService.success('Clipboard', 'Copied to clipboard.'); - } else { - notificationsService.error('Clipboard', 'Couldnt copy this data to clipboard.'); + /** + * Used to create a scoped watcher for a settings property on a blockObject. + */ + function createSettingsModelPropWatcher(blockObject, prop) { + return function () { + if (blockObject.settingsData[prop.alias] !== prop.value) { + // sync data: + blockObject.settingsData[prop.alias] = prop.value; + } + }; } - }; - /** - * @ngdoc method - * @name umbraco.services.clipboardService#copyArray - * @methodOf umbraco.services.clipboardService - * - * @param {string} type A string defining the type of data to storing, example: 'elementTypeArray', 'contentNodeArray' - * @param {string} aliases An array of strings defining the alias of the data to store, example: ['banana', 'apple'] - * @param {object} datas An array of objects containing the properties to be saved, example: [ElementType, ElementType, ...] + function createDataEntry(elementTypeKey, dataItems) { + var data = { + contentTypeKey: elementTypeKey, + udi: udiService.create('element') + }; + dataItems.push(data); + return data.udi; + } + function getDataByUdi(udi, dataItems) { + return dataItems.find(function (entry) { + return entry.udi === udi; + }) || null; + } + /** + * Set the udi and key property for the content item + * @param {any} contentData + * @param {any} udi + */ + function ensureUdiAndKey(contentData, udi) { + contentData.udi = udi; + // Change the content.key to the GUID part of the udi, else it's just random which we don't want, it must be consistent + contentData.key = udiService.getKey(udi); + } + /** + * Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property. + */ + var notSupportedProperties = [ + 'Umbraco.Tags', + 'Umbraco.UploadField', + 'Umbraco.ImageCropper', + 'Umbraco.NestedContent' + ]; + /** + * Formats the content apps and ensures unsupported property's have the notsupported view + * @param {any} scaffold + */ + function formatScaffoldData(scaffold) { + // deal with not supported props + scaffold.variants.forEach(function (variant) { + variant.tabs.forEach(function (tab) { + tab.properties.forEach(function (property) { + if (notSupportedProperties.indexOf(property.editor) !== -1) { + property.view = 'notsupported'; + } + }); + }); + }); + // could be empty in tests + if (!scaffold.apps) { + console.warn('No content apps found in scaffold'); + return scaffold; + } + // replace view of content app + var contentApp = scaffold.apps.find(function (entry) { + return entry.alias === 'umbContent'; + }); + if (contentApp) { + contentApp.view = 'views/common/infiniteeditors/blockeditor/blockeditor.content.html'; + } + // remove info app + var infoAppIndex = scaffold.apps.findIndex(function (entry) { + return entry.alias === 'umbInfo'; + }); + if (infoAppIndex >= 0) { + scaffold.apps.splice(infoAppIndex, 1); + } + return scaffold; + } + /** + * Creates a settings content app, we only want to do this if settings is present on the specific block. + * @param {any} contentModel + */ + function appendSettingsContentApp(contentModel, settingsName) { + if (!contentModel.apps) { + return; + } + // add the settings app + var settingsTab = { + 'name': settingsName, + 'alias': 'settings', + 'icon': 'icon-settings', + 'view': 'views/common/infiniteeditors/blockeditor/blockeditor.settings.html', + 'hasError': false + }; + contentModel.apps.push(settingsTab); + } + /** + * @ngdoc method + * @name constructor + * @methodOf umbraco.services.blockEditorModelObject + * @description Constructor of the model object used to handle Block Editor data. + * @param {object} propertyModelValue data object of the property editor, usually model.value. + * @param {string} propertyEditorAlias alias of the property. + * @param {object} blockConfigurations block configurations. + * @param {angular-scope} scopeOfExistance A local angularJS scope that exists as long as the data exists. + * @param {angular-scope} propertyEditorScope A local angularJS scope that represents the property editors scope. + * @returns {BlockEditorModelObject} A instance of BlockEditorModelObject. + */ + function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope) { + if (!propertyModelValue) { + throw new Error('propertyModelValue cannot be undefined, to ensure we keep the binding to the angular model we need minimum an empty object.'); + } + this.__watchers = []; + this.__labels = {}; + // ensure basic part of data-structure is in place: + this.value = propertyModelValue; + this.value.layout = this.value.layout || {}; + this.value.contentData = this.value.contentData || []; + this.value.settingsData = this.value.settingsData || []; + this.propertyEditorAlias = propertyEditorAlias; + this.blockConfigurations = blockConfigurations; + this.blockConfigurations.forEach(function (blockConfiguration) { + if (blockConfiguration.view != null && blockConfiguration.view !== '') { + blockConfiguration.view = umbRequestHelper.convertVirtualToAbsolutePath(blockConfiguration.view); + } + if (blockConfiguration.stylesheet != null && blockConfiguration.stylesheet !== '') { + blockConfiguration.stylesheet = umbRequestHelper.convertVirtualToAbsolutePath(blockConfiguration.stylesheet); + } + if (blockConfiguration.thumbnail != null && blockConfiguration.thumbnail !== '') { + blockConfiguration.thumbnail = umbRequestHelper.convertVirtualToAbsolutePath(blockConfiguration.thumbnail); + } + }); + this.scaffolds = []; + this.isolatedScope = scopeOfExistance.$new(true); + this.isolatedScope.blockObjects = {}; + this.__watchers.push(this.isolatedScope.$on('$destroy', this.destroy.bind(this))); + this.__watchers.push(propertyEditorScope.$on('formSubmittingFinalPhase', this.sync.bind(this))); + } + ; + BlockEditorModelObject.prototype = { + update: function update(propertyModelValue, propertyEditorScope) { + // clear watchers + this.__watchers.forEach(function (w) { + w(); + }); + delete this.__watchers; + // clear block objects + for (var key in this.isolatedScope.blockObjects) { + this.destroyBlockObject(this.isolatedScope.blockObjects[key]); + } + this.isolatedScope.blockObjects = {}; + // update our values + this.value = propertyModelValue; + this.value.layout = this.value.layout || {}; + this.value.contentData = this.value.contentData || []; + this.value.settingsData = this.value.settingsData || []; + // re-create the watchers + this.__watchers = []; + this.__watchers.push(this.isolatedScope.$on('$destroy', this.destroy.bind(this))); + this.__watchers.push(propertyEditorScope.$on('formSubmittingFinalPhase', this.sync.bind(this))); + }, + /** + * @ngdoc method + * @name getBlockConfiguration + * @methodOf umbraco.services.blockEditorModelObject + * @description Get block configuration object for a given contentElementTypeKey. + * @param {string} key contentElementTypeKey to recive the configuration model for. + * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentElementTypeKey isnt available in the current block configurations. + */ + getBlockConfiguration: function getBlockConfiguration(key) { + return this.blockConfigurations.find(function (bc) { + return bc.contentElementTypeKey === key; + }) || null; + }, + /** + * @ngdoc method + * @name load + * @methodOf umbraco.services.blockEditorModelObject + * @description Load the scaffolding models for the given configuration, these are needed to provide useful models for each block. + * @param {Object} blockObject BlockObject to receive data values from. + * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. + */ + load: function load() { + var self = this; + var tasks = []; + tasks.push(localizationService.localize('blockEditor_tabBlockSettings').then(function (settingsName) { + // self.__labels might not exists anymore, this happens if this instance has been destroyed before the load is complete. + if (self.__labels) { + self.__labels.settingsName = settingsName; + } + })); + var scaffoldKeys = []; + this.blockConfigurations.forEach(function (blockConfiguration) { + scaffoldKeys.push(blockConfiguration.contentElementTypeKey); + if (blockConfiguration.settingsElementTypeKey != null) { + scaffoldKeys.push(blockConfiguration.settingsElementTypeKey); + } + }); + // removing duplicates. + scaffoldKeys = scaffoldKeys.filter(function (value, index, self) { + return self.indexOf(value) === index; + }); + tasks.push(contentResource.getScaffoldByKeys(-20, scaffoldKeys).then(function (scaffolds) { + Object.values(scaffolds).forEach(function (scaffold) { + // self.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete. + if (self.scaffolds) { + self.scaffolds.push(formatScaffoldData(scaffold)); + } + }); + }).catch(function () { + })); + return $q.all(tasks); + }, + /** + * @ngdoc method + * @name getAvailableAliasesForBlockContent + * @methodOf umbraco.services.blockEditorModelObject + * @description Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings. + * @return {Array} array of strings representing alias. + */ + getAvailableAliasesForBlockContent: function getAvailableAliasesForBlockContent() { + var _this = this; + return this.blockConfigurations.map(function (blockConfiguration) { + var scaffold = _this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); + if (scaffold) { + return scaffold.contentTypeAlias; + } + }); + }, + /** + * @ngdoc method + * @name getAvailableBlocksForBlockPicker + * @methodOf umbraco.services.blockEditorModelObject + * @description Retrive a list of available blocks, the list containing object with the confirugation model(blockConfigModel) and the element type model(elementTypeModel). + * The purpose of this data is to provide it for the Block Picker. + * @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel. + */ + getAvailableBlocksForBlockPicker: function getAvailableBlocksForBlockPicker() { + var _this2 = this; + var blocks = []; + this.blockConfigurations.forEach(function (blockConfiguration) { + var scaffold = _this2.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); + if (scaffold) { + blocks.push({ + blockConfigModel: blockConfiguration, + elementTypeModel: scaffold.documentType + }); + } + }); + return blocks; + }, + /** + * @ngdoc method + * @name getScaffoldFromKey + * @methodOf umbraco.services.blockEditorModelObject + * @description Get scaffold model for a given contentTypeKey. + * @param {string} key contentTypeKey to recive the scaffold model for. + * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. + */ + getScaffoldFromKey: function getScaffoldFromKey(contentTypeKey) { + return this.scaffolds.find(function (o) { + return o.contentTypeKey === contentTypeKey; + }) || null; + }, + /** + * @ngdoc method + * @name getScaffoldFromAlias + * @methodOf umbraco.services.blockEditorModelObject + * @description Get scaffold model for a given contentTypeAlias, used by clipboardService. + * @param {string} alias contentTypeAlias to recive the scaffold model for. + * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. + */ + getScaffoldFromAlias: function getScaffoldFromAlias(contentTypeAlias) { + return this.scaffolds.find(function (o) { + return o.contentTypeAlias === contentTypeAlias; + }) || null; + }, + /** + * @ngdoc method + * @name getBlockObject + * @methodOf umbraco.services.blockEditorModelObject + * @description Retrieve a Block Object for the given layout entry. + * The Block Object offers the necessary data to display and edit a block. + * The Block Object setups live syncronization of content and settings models back to the data of your Property Editor model. + * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen. + * The ´BlockObject´ contains the following properties: + * - key {string}: runtime generated key, usefull for tracking of this object + * - content {Object}: Content model, the content data in a ElementType model. + * - settings {Object}: Settings model, the settings data in a ElementType model. + * - config {Object}: A local deep copy of the block configuration model. + * - label {string}: The label for this block. + * - updateLabel {Method}: Method to trigger an update of the label for this block. + * - data {Object}: A reference to the content data object from your property editor model. + * - settingsData {Object}: A reference to the settings data object from your property editor model. + * - layout {Object}: A refernce to the layout entry from your property editor model. + * @param {Object} layoutEntry the layout entry object to build the block model from. + * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasnt found for this block. + */ + getBlockObject: function getBlockObject(layoutEntry) { + var contentUdi = layoutEntry.contentUdi; + var dataModel = getDataByUdi(contentUdi, this.value.contentData); + if (dataModel === null) { + console.error('Couldn\'t find content data of ' + contentUdi); + return null; + } + var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey); + var contentScaffold = null; + if (blockConfiguration === null) { + console.warn('The block of ' + contentUdi + ' is not being initialized because its contentTypeKey(\'' + dataModel.contentTypeKey + '\') is not allowed for this PropertyEditor'); + } else { + contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); + if (contentScaffold === null) { + console.error('The block of ' + contentUdi + ' is not begin initialized cause its Element Type was not loaded.'); + } + } + if (blockConfiguration === null || contentScaffold === null) { + blockConfiguration = { + label: 'Unsupported', + unsupported: true + }; + } + var blockObject = {}; + // Set an angularJS cloneNode method, to avoid this object begin cloned. + blockObject.cloneNode = function () { + return null; // angularJS accept this as a cloned value as long as the + }; + blockObject.key = String.CreateGuid().replace(/-/g, ''); + blockObject.config = Utilities.copy(blockConfiguration); + if (blockObject.config.label && blockObject.config.label !== '') { + blockObject.labelInterpolator = $interpolate(blockObject.config.label); + } + blockObject.__scope = this.isolatedScope; + blockObject.updateLabel = _.debounce(function () { + // Check wether scope still exists, maybe object was destoyed in these seconds. + if (this.__scope) { + this.label = getBlockLabel(this); + this.__scope.$evalAsync(); + } + }.bind(blockObject), 10); + // make basics from scaffold + if (contentScaffold !== null) { + // We might not have contentScaffold + blockObject.content = Utilities.copy(contentScaffold); + ensureUdiAndKey(blockObject.content, contentUdi); + mapToElementModel(blockObject.content, dataModel); + } else { + blockObject.content = null; + } + blockObject.data = dataModel; + blockObject.layout = layoutEntry; + blockObject.__watchers = []; + if (blockConfiguration.settingsElementTypeKey) { + var settingsScaffold = this.getScaffoldFromKey(blockConfiguration.settingsElementTypeKey); + if (settingsScaffold !== null) { + if (!layoutEntry.settingsUdi) { + // if this block does not have settings data, then create it. This could happen because settings model has been added later than this content was created. + layoutEntry.settingsUdi = createDataEntry(blockConfiguration.settingsElementTypeKey, this.value.settingsData); + } + var settingsUdi = layoutEntry.settingsUdi; + var settingsData = getDataByUdi(settingsUdi, this.value.settingsData); + if (settingsData === null) { + console.error('Couldnt find settings data of ' + settingsUdi); + return null; + } + // the Settings model has been changed to a new Element Type. + // we need to update the settingsData with the new Content Type key + if (settingsData.contentTypeKey !== settingsScaffold.contentTypeKey) { + settingsData.contentTypeKey = settingsScaffold.contentTypeKey; + } + blockObject.settingsData = settingsData; + // make basics from scaffold + if (settingsScaffold !== null) { + // We might not have settingsScaffold + blockObject.settings = Utilities.copy(settingsScaffold); + ensureUdiAndKey(blockObject.settings, settingsUdi); + mapToElementModel(blockObject.settings, settingsData); + } else { + blockObject.settings = null; + } + // add settings content-app + appendSettingsContentApp(blockObject.content, this.__labels.settingsName); + } + } + blockObject.retrieveValuesFrom = function (content, settings) { + if (this.content !== null) { + mapElementValues(content, this.content); + } + if (this.config.settingsElementTypeKey !== null) { + mapElementValues(settings, this.settings); + } + }; + blockObject.sync = function () { + if (this.content !== null) { + mapToPropertyModel(this.content, this.data); + } + if (this.config.settingsElementTypeKey !== null) { + mapToPropertyModel(this.settings, this.settingsData); + } + }; + // first time instant update of label. + blockObject.label = getBlockLabel(blockObject); + // Add blockObject to our isolated scope to enable watching its values: + this.isolatedScope.blockObjects['_' + blockObject.key] = blockObject; + addWatchers(blockObject, this.isolatedScope); + addWatchers(blockObject, this.isolatedScope, true); + blockObject.destroy = function () { + // remove property value watchers: + this.__watchers.forEach(function (w) { + w(); + }); + delete this.__watchers; + // help carbage collector: + delete this.config; + delete this.layout; + delete this.data; + delete this.settingsData; + delete this.content; + delete this.settings; + // remove model from isolatedScope. + delete this.__scope.blockObjects['_' + this.key]; + // NOTE: It seems like we should call this.__scope.$destroy(); since that is the only way to remove a scope from it's parent, + // however that is not the case since __scope is actually this.isolatedScope which gets cleaned up when the outer scope is + // destroyed. If we do that here it breaks the scope chain and validation. + delete this.__scope; + // removes this method, making it impossible to destroy again. + delete this.destroy; + // lets remove the key to make things blow up if this is still referenced: + delete this.key; + }; + return blockObject; + }, + /** + * @ngdoc method + * @name removeDataAndDestroyModel + * @methodOf umbraco.services.blockEditorModelObject + * @description Removes the data and destroys the Block Model. + * Notice this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. + * @param {Object} blockObject The BlockObject to be removed and destroyed. + */ + removeDataAndDestroyModel: function removeDataAndDestroyModel(blockObject) { + var udi = blockObject.layout.contentUdi; + var settingsUdi = blockObject.layout.settingsUdi || null; + this.destroyBlockObject(blockObject); + this.removeDataByUdi(udi); + if (settingsUdi) { + this.removeSettingsByUdi(settingsUdi); + } + }, + /** + * @ngdoc method + * @name destroyBlockObject + * @methodOf umbraco.services.blockEditorModelObject + * @description Destroys the Block Model, but all data is kept. + * @param {Object} blockObject The BlockObject to be destroyed. + */ + destroyBlockObject: function destroyBlockObject(blockObject) { + blockObject.destroy(); + }, + /** + * @ngdoc method + * @name getLayout + * @methodOf umbraco.services.blockEditorModelObject + * @description Retrieve the layout object from this specific property editor model. + * @param {object} defaultStructure if no data exist the layout of your poerty editor will be set to this object. + * @return {Object} Layout object, structure depends on the model of your property editor. + */ + getLayout: function getLayout(defaultStructure) { + if (!this.value.layout[this.propertyEditorAlias]) { + this.value.layout[this.propertyEditorAlias] = defaultStructure; + } + return this.value.layout[this.propertyEditorAlias]; + }, + /** + * @ngdoc method + * @name create + * @methodOf umbraco.services.blockEditorModelObject + * @description Create a empty layout entry, notice the layout entry is not added to the property editors model layout object, since the layout sturcture depends on the property editor. + * @param {string} contentElementTypeKey the contentElementTypeKey of the block you wish to create, if contentElementTypeKey is not avaiable in the block configuration then ´null´ will be returned. + * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentElementTypeKey is unavaiaible. + */ + create: function create(contentElementTypeKey) { + var blockConfiguration = this.getBlockConfiguration(contentElementTypeKey); + if (blockConfiguration === null) { + return null; + } + var entry = { contentUdi: createDataEntry(contentElementTypeKey, this.value.contentData) }; + if (blockConfiguration.settingsElementTypeKey != null) { + entry.settingsUdi = createDataEntry(blockConfiguration.settingsElementTypeKey, this.value.settingsData); + } + return entry; + }, + /** + * @ngdoc method + * @name createFromElementType + * @methodOf umbraco.services.blockEditorModelObject + * @description Insert data from ElementType Model + * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration. + */ + createFromElementType: function createFromElementType(elementTypeDataModel) { + elementTypeDataModel = clipboardService.parseContentForPaste(elementTypeDataModel, clipboardService.TYPES.ELEMENT_TYPE); + var contentElementTypeKey = elementTypeDataModel.contentTypeKey; + var layoutEntry = this.create(contentElementTypeKey); + if (layoutEntry === null) { + return null; + } + var dataModel = getDataByUdi(layoutEntry.contentUdi, this.value.contentData); + if (dataModel === null) { + return null; + } + mapToPropertyModel(elementTypeDataModel, dataModel); + return layoutEntry; + }, + /** + * @ngdoc method + * @name createFromBlockData + * @methodOf umbraco.services.blockEditorModelObject + * @description Insert data from raw models + * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration. + */ + createFromBlockData: function createFromBlockData(blockData) { + blockData = clipboardService.parseContentForPaste(blockData, clipboardService.TYPES.BLOCK); + // As the blockData is a cloned object we can use its layout part for our layout entry. + var layoutEntry = blockData.layout; + if (layoutEntry === null) { + return null; + } + var blockConfiguration; + if (blockData.data) { + // Ensure that we support the alias: + blockConfiguration = this.getBlockConfiguration(blockData.data.contentTypeKey); + if (blockConfiguration === null) { + return null; + } + this.value.contentData.push(blockData.data); + } else { + // We do not have data, this cannot be succesful paste. + return null; + } + if (blockData.settingsData) { + // Ensure that we support the alias: + if (blockConfiguration.settingsElementTypeKey) { + // If we have settings for this Block Configuration, we need to check that they align, if we dont we do not want to fail. + if (blockConfiguration.settingsElementTypeKey === blockData.settingsData.contentTypeKey) { + this.value.settingsData.push(blockData.settingsData); + } else { + notificationsService.error('Clipboard', 'Couldn\'t paste because settings-data is not compatible.'); + return null; + } + } else { + // We do not have settings currently, so lets get rid of the settings part and move on with the paste. + delete layoutEntry.settingUdi; + } + } + return layoutEntry; + }, + /** + * @ngdoc method + * @name sync + * @methodOf umbraco.services.blockEditorModelObject + * @description Force immidiate update of the blockobject models to the property model. + */ + sync: function sync() { + for (var key in this.isolatedScope.blockObjects) { + this.isolatedScope.blockObjects[key].sync(); + } + }, + /** + * @ngdoc method + * @name removeDataByUdi + * @methodOf umbraco.services.blockEditorModelObject + * @description Removes the content data of a given UDI. + * Notice this method does not remove the block from your layout, this will need to be handled by the Property Editor since this services don't know about your layout structure. + * @param {string} udi The UDI of the content data to be removed. + */ + removeDataByUdi: function removeDataByUdi(udi) { + var index = this.value.contentData.findIndex(function (o) { + return o.udi === udi; + }); + if (index !== -1) { + this.value.contentData.splice(index, 1); + } + }, + /** + * @ngdoc method + * @name removeSettingsByUdi + * @methodOf umbraco.services.blockEditorModelObject + * @description Removes the settings data of a given UDI. + * Notice this method does not remove the settingsUdi from your layout, this will need to be handled by the Property Editor since this services don't know about your layout structure. + * @param {string} udi The UDI of the settings data to be removed. + */ + removeSettingsByUdi: function removeSettingsByUdi(udi) { + var index = this.value.settingsData.findIndex(function (o) { + return o.udi === udi; + }); + if (index !== -1) { + this.value.settingsData.splice(index, 1); + } + }, + /** + * @ngdoc method + * @name destroy + * @methodOf umbraco.services.blockEditorModelObject + * @description Notice you should not need to destroy the BlockEditorModelObject since it will automaticly be destroyed when the scope of existance gets destroyed. + */ + destroy: function destroy() { + this.__watchers.forEach(function (w) { + w(); + }); + for (var key in this.isolatedScope.blockObjects) { + this.destroyBlockObject(this.isolatedScope.blockObjects[key]); + } + delete this.__watchers; + delete this.value; + delete this.propertyEditorAlias; + delete this.blockConfigurations; + delete this.scaffolds; + this.isolatedScope.$destroy(); + delete this.isolatedScope; + delete this.destroy; + } + }; + return BlockEditorModelObject; + } + angular.module('umbraco.services').service('blockEditorModelObject', blockEditorModelObjectFactory); + }()); + 'use strict'; + /** + * @ngdoc service + * @name umbraco.services.clipboardService + * + * @requires notificationsService + * @requires eventsService + * + * @description + * Service to handle clipboard in general across the application. Responsible for handling the data both storing and retrive. + * The service has a set way for defining a data-set by a entryType and alias, which later will be used to retrive the posible entries for a paste scenario. + * + */ + function clipboardService($window, notificationsService, eventsService, localStorageService, iconHelper) { + var TYPES = {}; + TYPES.ELEMENT_TYPE = 'elementType'; + TYPES.BLOCK = 'block'; + TYPES.RAW = 'raw'; + TYPES.MEDIA = 'media'; + var clearPropertyResolvers = {}; + var pastePropertyResolvers = {}; + var clipboardTypeResolvers = {}; + clipboardTypeResolvers[TYPES.ELEMENT_TYPE] = function (element, propMethod) { + for (var t = 0; t < element.variants[0].tabs.length; t++) { + var tab = element.variants[0].tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + propMethod(prop, TYPES.ELEMENT_TYPE); + } + } + }; + clipboardTypeResolvers[TYPES.BLOCK] = function (block, propMethod) { + propMethod(block, TYPES.BLOCK); + if (block.data) { + Object.keys(block.data).forEach(function (key) { + if (key === 'udi' || key === 'contentTypeKey') { + return; + } + propMethod(block.data[key], TYPES.RAW); + }); + } + if (block.settingsData) { + Object.keys(block.settingsData).forEach(function (key) { + if (key === 'udi' || key === 'contentTypeKey') { + return; + } + propMethod(block.settingsData[key], TYPES.RAW); + }); + } /* + // Concept for supporting Block that contains other Blocks. + // Missing clarifications: + // How do we ensure that the inner blocks of a block is supported in the new scenario. Not that likely but still relevant, so considerations should be made. + if(block.references) { + // A Block clipboard entry can contain other Block Clipboard Entries, here we will make sure to resolve those identical to the main entry. + for (var r = 0; r < block.references.length; r++) { + clipboardTypeResolvers[TYPES.BLOCK](block.references[r], propMethod); + } + } + */ + }; + clipboardTypeResolvers[TYPES.RAW] = function (data, propMethod) { + for (var p = 0; p < data.length; p++) { + propMethod(data[p], TYPES.RAW); + } + }; + clipboardTypeResolvers[TYPES.MEDIA] = function (data, propMethod) { + }; + var STORAGE_KEY = 'umbClipboardService'; + var retriveStorage = function retriveStorage() { + if (localStorageService.isSupported === false) { + return null; + } + var dataJSON; + var dataString = localStorageService.get(STORAGE_KEY); + if (dataString != null) { + dataJSON = JSON.parse(dataString); + } + if (dataJSON == null) { + dataJSON = new Object(); + } + if (dataJSON.entries === undefined) { + dataJSON.entries = []; + } + return dataJSON; + }; + var saveStorage = function saveStorage(storage) { + var storageString = JSON.stringify(storage); + try { + // Check that we can parse the JSON: + var storageJSON = JSON.parse(storageString); + // Store the string: + localStorageService.set(STORAGE_KEY, storageString); + eventsService.emit('clipboardService.storageUpdate'); + return true; + } catch (e) { + return false; + } + return false; + }; + function resolvePropertyForStorage(prop, type) { + type = type || 'raw'; + var resolvers = clearPropertyResolvers[type]; + if (resolvers) { + for (var i = 0; i < resolvers.length; i++) { + resolvers[i](prop, resolvePropertyForStorage); + } + } + } + var prepareEntryForStorage = function prepareEntryForStorage(type, entryData, firstLevelClearupMethod) { + var cloneData = Utilities.copy(entryData); + if (firstLevelClearupMethod != undefined) { + firstLevelClearupMethod(cloneData); + } + var typeResolver = clipboardTypeResolvers[type]; + if (typeResolver) { + typeResolver(cloneData, resolvePropertyForStorage); + } else { + console.warn('Umbraco.service.clipboardService has no type resolver for \'' + type + '\'.'); + } + return cloneData; + }; + var isEntryCompatible = function isEntryCompatible(entry, type, allowedAliases) { + return entry.type === type && (allowedAliases === null || entry.alias && allowedAliases.filter(function (allowedAlias) { + return allowedAlias === entry.alias; + }).length > 0 || entry.aliases && entry.aliases.filter(function (entryAlias) { + return allowedAliases.filter(function (allowedAlias) { + return allowedAlias === entryAlias; + }).length > 0; + }).length === entry.aliases.length); + }; + function resolvePropertyForPaste(prop, type) { + type = type || 'raw'; + var resolvers = pastePropertyResolvers[type]; + if (resolvers) { + for (var i = 0; i < resolvers.length; i++) { + resolvers[i](prop, resolvePropertyForPaste); + } + } + } + var service = {}; + /** + * Default types to store in clipboard. + */ + service.TYPES = TYPES; + /** + * @ngdoc method + * @name umbraco.services.clipboardService#parseContentForPaste + * @methodOf umbraco.services.clipboardService + * + * @param {object} function The content entry to be cloned and resolved. + * + * @description + * Executed registered property resolvers for inner properties, to be done on pasting a clipbaord content entry. + * + */ + service.parseContentForPaste = function (pasteEntryData, type) { + var cloneData = Utilities.copy(pasteEntryData); + var typeResolver = clipboardTypeResolvers[type]; + if (typeResolver) { + typeResolver(cloneData, resolvePropertyForPaste); + } else { + console.warn('Umbraco.service.clipboardService has no type resolver for \'' + type + '\'.'); + } + return cloneData; + }; + /** + * @ngdoc method + * @name umbraco.services.clipboardService#registrerClearPropertyResolver + * @methodOf umbraco.services.clipboardService + * + * @param {string} function A method executed for every property and inner properties copied. + * @param {string} string A string representing the property type format for this resolver to execute for. + * + * @description + * Executed for all properties of given type when performing a copy action. + * + * @deprecated Incorrect spelling please use 'registerClearPropertyResolver' + */ + service.registrerClearPropertyResolver = function (resolver, type) { + this.registerClearPropertyResolver(resolver, type); + }; + /** + * @ngdoc method + * @name umbraco.services.clipboardService#registerClearPropertyResolver + * @methodOf umbraco.services.clipboardService + * + * @param {method} function A method executed for every property and inner properties copied. Notice the method both needs to deal with retriving a property object {alias:..editor:..value:... ,...} and the raw property value as if the property is an inner property of a nested property. + * @param {string} string A string representing the property type format for this resolver to execute for. + * + * @description + * Executed for all properties of given type when performing a copy action. + */ + service.registerClearPropertyResolver = function (resolver, type) { + type = type || 'raw'; + if (!clearPropertyResolvers[type]) { + clearPropertyResolvers[type] = []; + } + clearPropertyResolvers[type].push(resolver); + }; + /** + * @ngdoc method + * @name umbraco.services.clipboardService#registerPastePropertyResolver + * @methodOf umbraco.services.clipboardService + * + * @param {method} function A method executed for every property and inner properties copied. Notice the method both needs to deal with retriving a property object {alias:..editor:..value:... ,...} and the raw property value as if the property is an inner property of a nested property. + * @param {string} string A string representing the property type format for this resolver to execute for. + * + * @description + * Executed for all properties of given type when performing a paste action. + */ + service.registerPastePropertyResolver = function (resolver, type) { + type = type || 'raw'; + if (!pastePropertyResolvers[type]) { + pastePropertyResolvers[type] = []; + } + pastePropertyResolvers[type].push(resolver); + }; + /** + * @ngdoc method + * @name umbraco.services.clipboardService#registrerTypeResolvers + * @methodOf umbraco.services.clipboardService + * + * @param {method} function A method for to execute resolvers for each properties of the specific type. + * @param {string} string A string representing the content type format for this resolver to execute for. + * + * @description + * Executed for all properties including inner properties when performing a paste action. + */ + service.registrerTypeResolvers = function (resolver, type) { + if (!clipboardTypeResolvers[type]) { + clipboardTypeResolvers[type] = []; + } + clipboardTypeResolvers[type].push(resolver); + }; + /** + * @ngdoc method + * @name umbraco.services.clipboardService#copy + * @methodOf umbraco.services.clipboardService + * + * @param {string} type A string defining the type of data to storing, example: 'elementType', 'contentNode' + * @param {string} alias A string defining the alias of the data to store, example: 'product' + * @param {object} entry A object containing the properties to be saved, this could be the object of a ElementType, ContentNode, ... + * @param {string} displayLabel (optional) A string swetting the label to display when showing paste entries. + * @param {string} displayIcon (optional) A string setting the icon to display when showing paste entries. + * @param {string} uniqueKey (optional) A string prodiving an identifier for this entry, existing entries with this key will be removed to ensure that you only have the latest copy of this data. + * + * @description + * Saves a single JS-object with a type and alias to the clipboard. + */ + service.copy = function (type, alias, data, displayLabel, displayIcon, uniqueKey, firstLevelClearupMethod) { + var storage = retriveStorage(); + displayLabel = displayLabel || data.name; + displayIcon = displayIcon || iconHelper.convertFromLegacyIcon(data.icon); + uniqueKey = uniqueKey || data.key || console.error('missing unique key for this content'); + // remove previous copies of this entry: + storage.entries = storage.entries.filter(function (entry) { + return entry.unique !== uniqueKey; + }); + var entry = { + unique: uniqueKey, + type: type, + alias: alias, + data: prepareEntryForStorage(type, data, firstLevelClearupMethod), + label: displayLabel, + icon: displayIcon, + date: Date.now() + }; + storage.entries.push(entry); + if (saveStorage(storage) === true) { + notificationsService.success('Clipboard', 'Copied to clipboard.'); + } else { + notificationsService.error('Clipboard', 'Couldnt copy this data to clipboard.'); + } + }; + /** + * @ngdoc method + * @name umbraco.services.clipboardService#copyArray + * @methodOf umbraco.services.clipboardService + * + * @param {string} type A string defining the type of data to storing, example: 'elementTypeArray', 'contentNodeArray' + * @param {string} aliases An array of strings defining the alias of the data to store, example: ['banana', 'apple'] + * @param {object} datas An array of objects containing the properties to be saved, example: [ElementType, ElementType, ...] * @param {string} displayLabel A string setting the label to display when showing paste entries. * @param {string} displayIcon A string setting the icon to display when showing paste entries. * @param {string} uniqueKey A string prodiving an identifier for this entry, existing entries with this key will be removed to ensure that you only have the latest copy of this data. @@ -1007,10 +2173,13 @@ * Saves a single JS-object with a type and alias to the clipboard. */ service.copyArray = function (type, aliases, datas, displayLabel, displayIcon, uniqueKey, firstLevelClearupMethod) { + if (type === 'elementTypeArray') { + type = 'elementType'; + } var storage = retriveStorage(); // Clean up each entry var copiedDatas = datas.map(function (data) { - return prepareEntryForStorage(data, firstLevelClearupMethod); + return prepareEntryForStorage(type, data, firstLevelClearupMethod); }); // remove previous copies of this entry: storage.entries = storage.entries.filter(function (entry) { @@ -1022,7 +2191,8 @@ aliases: aliases, data: copiedDatas, label: displayLabel, - icon: displayIcon + icon: displayIcon, + date: Date.now() }; storage.entries.push(entry); if (saveStorage(storage) === true) { @@ -1066,7 +2236,7 @@ * * @param {string} type A string defining the type of data to recive. * @param {string} aliases A array of strings providing the alias of the data you want to recive. - * + * * @description * Returns an array of entries matching the given type and one of the provided aliases. */ @@ -1085,7 +2255,7 @@ * * @param {string} type A string defining the type of data to recive. * @param {string} aliases A array of strings providing the alias of the data you want to recive. - * + * * @description * Returns an array of data of entries matching the given type and one of the provided aliases. */ @@ -1101,23 +2271,117 @@ * * @param {string} type A string defining the type of data to remove. * @param {string} aliases A array of strings providing the alias of the data you want to remove. - * + * * @description * Removes entries matching the given type and one of the provided aliases. */ - service.clearEntriesOfType = function (type, allowedAliases) { - var storage = retriveStorage(); - // Find entries that are NOT fulfilling the criteria for this nodeType and nodeTypesAliases. - var filteretEntries = storage.entries.filter(function (entry) { - return !isEntryCompatible(entry, type, allowedAliases); - }); - storage.entries = filteretEntries; - saveStorage(storage); + service.clearEntriesOfType = function (type, allowedAliases) { + var storage = retriveStorage(); + // Find entries that are NOT fulfilling the criteria for this nodeType and nodeTypesAliases. + var filteretEntries = storage.entries.filter(function (entry) { + return !isEntryCompatible(entry, type, allowedAliases); + }); + storage.entries = filteretEntries; + saveStorage(storage); + }; + var emitClipboardStorageUpdate = _.debounce(function (e) { + eventsService.emit('clipboardService.storageUpdate'); + }, 1000); + // Fires if LocalStorage was changed from another tab than this one. + $window.addEventListener('storage', emitClipboardStorageUpdate); + return service; + } + angular.module('umbraco.services').factory('clipboardService', clipboardService); + 'use strict'; + /** +* @ngdoc service +* @name umbraco.services.contentAppHelper +* @description A helper service for content app related functions. +**/ + function contentAppHelper() { + var service = {}; + /** + * Default known content based apps. + */ + service.CONTENT_BASED_APPS = [ + 'umbContent', + 'umbInfo', + 'umbListView' + ]; + /** + * @ngdoc method + * @name umbraco.services.contentAppHelper#isContentBasedApp + * @methodOf umbraco.services.contentAppHelper + * + * @param {object} app A content app to check + * + * @description + * Determines whether the supplied content app is a known content based app + * + */ + service.isContentBasedApp = function (app) { + return service.CONTENT_BASED_APPS.indexOf(app.alias) !== -1; }; return service; } - angular.module('umbraco.services').factory('clipboardService', clipboardService); + angular.module('umbraco.services').factory('contentAppHelper', contentAppHelper); 'use strict'; + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); + } + function _nonIterableSpread() { + throw new TypeError('Invalid attempt to spread non-iterable instance'); + } + function _iterableToArray(iter) { + if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === '[object Arguments]') + return Array.from(iter); + } + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } + } + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); + } + function _nonIterableRest() { + throw new TypeError('Invalid attempt to destructure non-iterable instance'); + } + function _iterableToArrayLimit(arr, i) { + if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === '[object Arguments]')) { + return; + } + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + if (i && _arr.length === i) + break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i['return'] != null) + _i['return'](); + } finally { + if (_d) + throw _e; + } + } + return _arr; + } + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) + return arr; + } /** * @ngdoc service * @name umbraco.services.contentEditingHelper @@ -1127,7 +2391,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorState, notificationsService, navigationService, localizationService, serverValidationManager, formHelper) { function isValidIdentifier(id) { //empty id <= 0 - if (angular.isNumber(id)) { + if (Utilities.isNumber(id)) { if (id === 0) { return false; } @@ -1145,12 +2409,35 @@ } return true; } + function showNotificationsForModelsState(ms, messageType) { + messageType = messageType || 2; + for (var _i = 0, _Object$entries = Object.entries(ms); _i < _Object$entries.length; _i++) { + var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2), key = _Object$entries$_i[0], value = _Object$entries$_i[1]; + var errorMsg = value[0]; + // if the error message is json it's a complex editor validation response that we need to parse + if (Utilities.isString(errorMsg) && errorMsg.startsWith('[') || Utilities.isArray(errorMsg)) { + // flatten the json structure, create validation paths for each property and add each as a property error + var idsToErrors = serverValidationManager.parseComplexEditorError(errorMsg, ''); + idsToErrors.forEach(function (x) { + if (x.modelState) { + showNotificationsForModelsState(x.modelState, messageType); + } + }); + } else if (value[0]) { + notificationsService.showNotification({ + type: messageType, + header: 'Validation', + message: value[0] + }); + } + } + } return { //TODO: We need to move some of this to formHelper for saving, too many editors use this method for saving when this entire //service should only be used for content/media/members /** Used by the content editor and mini content editor to perform saving operations */ contentEditorPerformSave: function contentEditorPerformSave(args) { - if (!angular.isObject(args)) { + if (!Utilities.isObject(args)) { throw 'args must be an object'; } if (!args.scope) { @@ -1180,10 +2467,15 @@ var self = this; //we will use the default one for content if not specified var _rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; - if (formHelper.submitForm({ - scope: args.scope, - action: args.action - })) { + var formSubmitOptions = { + scope: args.scope, + action: args.action + }; + if (args.skipValidation === true) { + formSubmitOptions.skipValidation = true; + formSubmitOptions.keepServerValidation = true; + } + if (formHelper.submitForm(formSubmitOptions)) { return args.saveMethod(args.content, args.create, fileManager.getFiles(), args.showNotifications).then(function (data) { formHelper.resetForm({ scope: args.scope }); if (!args.infiniteMode) { @@ -1203,15 +2495,23 @@ } return $q.resolve(data); }, function (err) { + formHelper.resetForm({ + scope: args.scope, + hasErrors: true + }); self.handleSaveError({ showNotifications: args.showNotifications, softRedirect: args.softRedirect, err: err, + action: args.action, rebindCallback: function rebindCallback() { - _rebindCallback.apply(self, [ - args.content, - err.data - ]); + // if the error contains data, we want to map that back as we want to continue editing this save. Especially important when the content is new as the returned data will contain ID etc. + if (err.data) { + _rebindCallback.apply(self, [ + args.content, + err.data + ]); + } } }); //update editor state to what is current @@ -1232,7 +2532,7 @@ }; // first check if tab is already added var foundInfoTab = false; - angular.forEach(tabs, function (tab) { + tabs.forEach(function (tab) { if (tab.id === infoTab.id && tab.alias === infoTab.alias) { foundInfoTab = true; } @@ -1245,11 +2545,44 @@ }); } }, + registerGenericTab: function registerGenericTab(groups) { + if (!groups) { + return; + } + var hasGenericTab = groups.find(function (group) { + return group.isGenericTab; + }); + if (hasGenericTab) { + return; + } + var isRootGroup = function isRootGroup(group) { + return group.type === 0 && group.parentAlias === null; + }; + var hasRootGroups = groups.filter(function (group) { + return isRootGroup(group); + }).length > 0; + if (!hasRootGroups) { + return; + } + var genericTab = { + isGenericTab: true, + type: 1, + label: 'Generic', + key: String.CreateGuid(), + alias: null, + parentAlias: null, + properties: [] + }; + localizationService.localize('general_generic').then(function (value) { + genericTab.label = value; + groups.unshift(genericTab); + }); + }, /** 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 configureContentEditorButtons(args) { - if (!angular.isObject(args)) { + if (!Utilities.isObject(args)) { throw 'args must be an object'; } if (!args.content) { @@ -1371,7 +2704,7 @@ } } // if publishing is allowed also allow schedule publish - // we add this manually becuase it doesn't have a permission so it wont + // we add this manually becuase it doesn't have a permission so it wont // get picked up by the loop through permissions if (_.contains(args.content.allowedActions, 'U')) { buttons.subButtons.push(createButtonDefinition('SCHEDULE')); @@ -1417,9 +2750,11 @@ * * @description * Returns a id for the variant that is unique between all variants on the content + * Note "invariant" is used for the invariant culture, + * "null" is used for the NULL segment */ buildCompositeVariantId: function buildCompositeVariantId(variant) { - return (variant.language ? variant.language.culture : 'invariant') + '_' + (variant.segment ? variant.segment : ''); + return (variant.language ? variant.language.culture : 'invariant') + '_' + (variant.segment ? variant.segment : 'null'); }, /** * @ngdoc method @@ -1621,7 +2956,7 @@ //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(origProp.onValueChanged)) { + if (Utilities.isFunction(origProp.onValueChanged)) { //send the newVal + oldVal origProp.onValueChanged(origProp.value, origVal); } @@ -1655,17 +2990,20 @@ if (args.err.data && args.err.data.ModelState) { //wire up the server validation errs formHelper.handleServerValidation(args.err.data.ModelState); + var messageType = 2; + //error + if (args.action === 'save') { + messageType = 4; //warning + } //add model state errors to notifications if (args.showNotifications) { - for (var e in args.err.data.ModelState) { - notificationsService.error('Validation', args.err.data.ModelState[e][0]); - } + showNotificationsForModelsState(args.err.data.ModelState, messageType); } if (!this.redirectToCreatedContent(args.err.data.id, args.softRedirect) || args.softRedirect) { // If we are not redirecting it's because this is not newly created content, else in some cases we are - // soft-redirecting which means the URL will change but the route wont (i.e. creating content). + // soft-redirecting which means the URL will change but the route wont (i.e. creating content). // In this case we need to detect what properties have changed and re-bind them with the server data. - if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { + if (args.rebindCallback && Utilities.isFunction(args.rebindCallback)) { args.rebindCallback(); } // In this case notify all validators (don't clear the server validations though since we need to maintain their state because of @@ -1701,9 +3039,9 @@ } if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id, args.softRedirect) || args.softRedirect) { // If we are not redirecting it's because this is not newly created content, else in some cases we are - // soft-redirecting which means the URL will change but the route wont (i.e. creating content). + // soft-redirecting which means the URL will change but the route wont (i.e. creating content). // In this case we need to detect what properties have changed and re-bind them with the server data. - if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { + if (args.rebindCallback && Utilities.isFunction(args.rebindCallback)) { args.rebindCallback(); } } @@ -1727,7 +3065,10 @@ // 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 - navigationService.clearSearch(['cculture']); + navigationService.clearSearch([ + 'cculture', + 'csegment' + ]); if (softRedirect) { navigationService.setSoftRedirect(); } @@ -1759,11 +3100,98 @@ //don't add a browser history for this $location.replace(); return true; + }, + /** + * @ngdoc function + * @name umbraco.services.contentEditingHelper#sortVariants + * @methodOf umbraco.services.contentEditingHelper + * @function + * + * @description + * Sorts the variants so default language is shown first. Mandatory languages are shown next and all other underneath. Both Mandatory and non mandatory languages are + * sorted in the following groups 'Published', 'Draft', 'Not Created'. Within each of those groups the variants are + * sorted by the language display name. + * + */ + sortVariants: function sortVariants(a, b) { + var statesOrder = { + 'PublishedPendingChanges': 1, + 'Published': 1, + 'Draft': 2, + 'NotCreated': 3 + }; + var compareDefault = function compareDefault(a, b) { + return (a.language && a.language.isDefault ? -1 : 1) - (b.language && b.language.isDefault ? -1 : 1); + }; + // Make sure mandatory variants goes on top, unless they are published, cause then they already goes to the top and then we want to mix them with other published variants. + var compareMandatory = function compareMandatory(a, b) { + return a.state === 'PublishedPendingChanges' || a.state === 'Published' ? 0 : (a.language && a.language.isMandatory ? -1 : 1) - (b.language && b.language.isMandatory ? -1 : 1); + }; + var compareState = function compareState(a, b) { + return (statesOrder[a.state] || 99) - (statesOrder[b.state] || 99); + }; + var compareName = function compareName(a, b) { + return a.displayName.localeCompare(b.displayName); + }; + return compareDefault(a, b) || compareMandatory(a, b) || compareState(a, b) || compareName(a, b); + }, + /** + * @ngdoc function + * @name umbraco.services.contentEditingHelper#getSortedVariantsAndSegments + * @methodOf umbraco.services.contentEditingHelper + * @function + * + * @description + * Returns an array of variants and segments sorted by the rules in the sortVariants method. + * A variant language is followed by its segments in the array. If a segment doesn't have a parent variant it is + * added to the end of the array. + * + */ + getSortedVariantsAndSegments: function getSortedVariantsAndSegments(variantsAndSegments) { + var _this = this; + var sortedVariants = variantsAndSegments.filter(function (variant) { + return !variant.segment; + }).sort(this.sortVariants); + var variantsWithSegments = variantsAndSegments.filter(function (variant) { + return variant.segment; + }); + var sortedAvailableVariants = []; + sortedVariants.forEach(function (variant) { + var sortedMatchedSegments = variantsWithSegments.filter(function (segment) { + return segment.language && variant.language && segment.language.culture === variant.language.culture; + }).sort(_this.sortVariants); + // remove variants for this culture + variantsWithSegments = variantsWithSegments.filter(function (segment) { + return !segment.language || segment.language && variant.language && segment.language.culture !== variant.language.culture; + }); + sortedAvailableVariants = [].concat(_toConsumableArray(sortedAvailableVariants), [variant], _toConsumableArray(sortedMatchedSegments)); + }); + // if we have segments without a parent language variant we need to add the remaining variantsWithSegments to the array + sortedAvailableVariants = [].concat(_toConsumableArray(sortedAvailableVariants), _toConsumableArray(variantsWithSegments.sort(this.sortVariants))); + return sortedAvailableVariants; } }; } angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper); 'use strict'; + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); + } + function _nonIterableSpread() { + throw new TypeError('Invalid attempt to spread non-iterable instance'); + } + function _iterableToArray(iter) { + if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === '[object Arguments]') + return Array.from(iter); + } + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } + } /** * @ngdoc service * @name umbraco.services.contentTypeHelper @@ -1771,10 +3199,100 @@ **/ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $injector, $q) { var contentTypeHelperService = { + TYPE_GROUP: 0, + TYPE_TAB: 1, + isAliasUnique: function isAliasUnique(groups, alias) { + return groups.find(function (group) { + return group.alias === alias; + }) ? false : true; + }, + createUniqueAlias: function createUniqueAlias(groups, alias) { + var i = 1; + while (this.isAliasUnique(groups, alias + i.toString()) === false) { + i++; + } + return alias + i.toString(); + }, + generateLocalAlias: function generateLocalAlias(name) { + return name ? name.toUmbracoAlias() : String.CreateGuid(); + }, + getLocalAlias: function getLocalAlias(alias) { + var lastIndex = alias.lastIndexOf('/'); + return lastIndex === -1 ? alias : alias.substring(lastIndex + 1); + }, + updateLocalAlias: function updateLocalAlias(alias, localAlias) { + var parentAlias = this.getParentAlias(alias); + return parentAlias == null || parentAlias === '' ? localAlias : parentAlias + '/' + localAlias; + }, + getParentAlias: function getParentAlias(alias) { + if (alias) { + var lastIndex = alias.lastIndexOf('/'); + return lastIndex === -1 ? null : alias.substring(0, lastIndex); + } + return null; + }, + updateParentAlias: function updateParentAlias(alias, parentAlias) { + var localAlias = this.getLocalAlias(alias); + return parentAlias == null || parentAlias === '' ? localAlias : parentAlias + '/' + localAlias; + }, + updateDescendingAliases: function updateDescendingAliases(groups, oldParentAlias, newParentAlias) { + var _this = this; + groups.forEach(function (group) { + var parentAlias = _this.getParentAlias(group.alias); + if (parentAlias === oldParentAlias) { + var oldAlias = group.alias, newAlias = _this.updateParentAlias(oldAlias, newParentAlias); + group.alias = newAlias; + group.parentAlias = newParentAlias; + _this.updateDescendingAliases(groups, oldAlias, newAlias); + } + }); + }, + defineParentAliasOnGroups: function defineParentAliasOnGroups(groups) { + var _this2 = this; + groups.forEach(function (group) { + group.parentAlias = _this2.getParentAlias(group.alias); + }); + }, + relocateDisorientedGroups: function relocateDisorientedGroups(groups) { + var _this3 = this; + var existingAliases = groups.map(function (group) { + return group.alias; + }); + existingAliases.push(null); + var disorientedGroups = groups.filter(function (group) { + return existingAliases.indexOf(group.parentAlias) === -1; + }); + disorientedGroups.forEach(function (group) { + var oldAlias = group.alias, newAlias = _this3.updateParentAlias(oldAlias, null); + group.alias = newAlias; + group.parentAlias = null; + _this3.updateDescendingAliases(groups, oldAlias, newAlias); + }); + }, + convertGroupToTab: function convertGroupToTab(groups, group) { + var _this4 = this; + var tabs = groups.filter(function (group) { + return group.type === _this4.TYPE_TAB; + }).sort(function (a, b) { + return a.sortOrder - b.sortOrder; + }); + var nextSortOrder = tabs && tabs.length > 0 ? tabs[tabs.length - 1].sortOrder + 1 : 0; + group.convertingToTab = true; + group.type = this.TYPE_TAB; + var newAlias = this.generateLocalAlias(group.name); + // when checking for alias uniqueness we need to exclude the current group or the alias would get a + 1 + var otherGroups = _toConsumableArray(groups).filter(function (groupCopy) { + return !groupCopy.convertingToTab; + }); + group.alias = this.isAliasUnique(otherGroups, newAlias) ? newAlias : this.createUniqueAlias(otherGroups, newAlias); + group.parentAlias = null; + group.sortOrder = nextSortOrder; + group.convertingToTab = false; + }, createIdArray: function createIdArray(array) { var newArray = []; - angular.forEach(array, function (arrayItem) { - if (angular.isObject(arrayItem)) { + array.forEach(function (arrayItem) { + if (Utilities.isObject(arrayItem)) { newArray.push(arrayItem.id); } else { newArray.push(arrayItem); @@ -1854,11 +3372,11 @@ 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) { + compositeContentType.groups.forEach(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) { + compositionGroup.properties.forEach(function (property) { dataTypeResource.getById(property.dataTypeId).then(function (dataType) { property.dataTypeIcon = dataType.icon; property.dataTypeName = dataType.name; @@ -1867,14 +3385,14 @@ // set inherited state on tab compositionGroup.inherited = true; // set inherited state on properties - angular.forEach(compositionGroup.properties, function (compositionProperty) { + compositionGroup.properties.forEach(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) { + contentType.groups.forEach(function (contentTypeGroup) { + if (contentTypeGroup.name === compositionGroup.name && contentTypeGroup.type === compositionGroup.type) { // set flag to show if properties has been merged into a tab compositionGroup.groupIsMerged = true; // make group inherited @@ -1898,7 +3416,7 @@ // get sort order from composition contentTypeGroup.sortOrder = compositionGroup.sortOrder; // splice group to the top of the array - var contentTypeGroupCopy = angular.copy(contentTypeGroup); + var contentTypeGroupCopy = Utilities.copy(contentTypeGroup); var index = contentType.groups.indexOf(contentTypeGroup); contentType.groups.splice(index, 1); contentType.groups.unshift(contentTypeGroupCopy); @@ -1928,7 +3446,7 @@ }, splitCompositeContentType: function splitCompositeContentType(contentType, compositeContentType) { var groups = []; - angular.forEach(contentType.groups, function (contentTypeGroup) { + contentType.groups.forEach(function (contentTypeGroup) { if (contentTypeGroup.tabState !== 'init') { var idIndex = contentTypeGroup.parentTabContentTypes.indexOf(compositeContentType.id); var nameIndex = contentTypeGroup.parentTabContentTypeNames.indexOf(compositeContentType.name); @@ -1936,7 +3454,7 @@ if (idIndex !== -1) { var properties = []; // remove all properties from composite content type - angular.forEach(contentTypeGroup.properties, function (property) { + contentTypeGroup.properties.forEach(function (property) { if (property.contentTypeId !== compositeContentType.id) { properties.push(property); } @@ -1951,8 +3469,7 @@ contentTypeGroup.inherited = false; } // remove group if there are no properties left - if (contentTypeGroup.properties.length > 1) { - //contentType.groups.splice(groupIndex, 1); + if (contentTypeGroup.properties.length > 0) { groups.push(contentTypeGroup); } } else { @@ -1968,7 +3485,7 @@ }, updatePropertiesSortOrder: function updatePropertiesSortOrder(properties) { var sortOrder = 0; - angular.forEach(properties, function (property) { + properties.forEach(function (property) { if (!property.inherited && property.propertyState !== 'init') { property.sortOrder = sortOrder; } @@ -2006,6 +3523,41 @@ 'id': id }; array.push(placeholder); + }, + rebindSavedContentType: function rebindSavedContentType(contentType, savedContentType) { + // The saved content type might have updated values (eg. new IDs/keys), so make sure the view model is updated + contentType.ModelState = savedContentType.ModelState; + contentType.id = savedContentType.id; + contentType.groups.forEach(function (group) { + if (!group.alias) + return; + var k = 0; + while (k < savedContentType.groups.length && savedContentType.groups[k].alias != group.alias) { + k++; + } + if (k == savedContentType.groups.length) { + group.id = 0; + return; + } + var savedGroup = savedContentType.groups[k]; + group.id = savedGroup.id; + group.key = savedGroup.key; + group.contentTypeId = savedGroup.contentTypeId; + 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; + }); + }); } }; return contentTypeHelperService; @@ -2056,25 +3608,19 @@ ratio: ratio }; }, - scaleToMaxSize: function scaleToMaxSize(srcWidth, srcHeight, maxSize) { - var retVal = { - height: srcHeight, - width: srcWidth + scaleToMaxSize: function scaleToMaxSize(srcWidth, srcHeight, maxWidth, maxHeight) { + // fallback to maxHeight: + maxHeight = maxHeight || maxWidth; + // get smallest ratio, if ratio exceeds 1 we will not scale(hence we parse 1 as the maximum allowed ratio) + var ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight, 1); + return { + width: srcWidth * ratio, + height: srcHeight * ratio }; - 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 + //offset is just to push the image position a number of pixels from top,left convertToStyle: function convertToStyle(coordinates, originalSize, viewPort, offset) { var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset); var _offset = offset || 0; @@ -2109,15 +3655,10 @@ 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; - } - } + crop.x1 = Math.max(x1_px / image.width, 0); + crop.y1 = Math.max(y1_px / image.height, 0); + crop.x2 = Math.max(x2_px / image.width, 0); + crop.y2 = Math.max(y2_px / image.height, 0); return crop; }, alignToCoordinates: function alignToCoordinates(image, center, viewport) { @@ -2167,11 +3708,12 @@ for (var i = 0; i < preVals.length; i++) { preValues.push({ hideLabel: preVals[i].hideLabel, - alias: preVals[i].key, + alias: preVals[i].key != undefined ? preVals[i].key : preVals[i].alias, description: preVals[i].description, label: preVals[i].label, view: preVals[i].view, - value: preVals[i].value + value: preVals[i].value, + config: preVals[i].config }); } return preValues; @@ -2407,6 +3949,7 @@ When building a custom infinite editor view you can use the same components as a var editorsKeyboardShorcuts = []; var editors = []; var isEnabled = true; + var lastElementInFocus = null; // events for backdrop eventsService.on('appState.backdrop', function (name, args) { if (args.show === true) { @@ -2421,7 +3964,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Method to return all open editors + * Method to return all open editors. */ function getEditors() { return editors; @@ -2433,7 +3976,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Method to return the number of open editors + * Method to return the number of open editors. */ function getNumberOfEditors() { return editors.length; @@ -2479,11 +4022,11 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Method to open a new editor in infinite editing + * Method to open a new editor in infinite editing. * - * @param {Object} editor rendering options - * @param {String} editor.view Path to view - * @param {String} editor.size Sets the size of the editor ("small" || "medium"). If nothing is set it will use full width. + * @param {object} editor rendering options. + * @param {string} editor.view URL to view. + * @param {string} editor.size Sets the size of the editor (`small` or `medium`). If nothing is set it will use full width. */ function open(editor) { /* keyboard shortcuts will be overwritten by the new infinite editor @@ -2491,6 +4034,11 @@ When building a custom infinite editor view you can use the same components as a when the infinite editor closes */ unbindKeyboardShortcuts(); + // if this is the first editor layer, save the currently focused element + // so we can re-apply focus to it once all the editor layers are closed + if (editors.length === 0) { + lastElementInFocus = document.activeElement; + } // set flag so we know when the editor is open in "infinite mode" editor.infiniteMode = true; editors.push(editor); @@ -2506,7 +4054,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Method to close the latest opened editor + * Method to close the latest opened editor. */ function close() { // close last opened editor @@ -2523,6 +4071,9 @@ When building a custom infinite editor view you can use the same components as a $timeout(function () { // rebind keyboard shortcuts for the new editor in focus rebindKeyboardShortcuts(); + if (editors.length === 0 && lastElementInFocus) { + lastElementInFocus.focus(); + } }, 0); } /** @@ -2531,7 +4082,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Method to close all open editors + * Method to close all open editors. */ function closeAll() { editors = []; @@ -2547,18 +4098,18 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a content editor in infinite editing, the submit callback returns the updated content item - * @param {Object} editor rendering options - * @param {String} editor.id The id of the content item - * @param {Boolean} editor.create Create new content item - * @param {Function} editor.submit Callback function when the publish and close button is clicked. Returns the editor model object - * @param {Function} editor.close Callback function when the close button is clicked. - * @param {String} editor.parentId If editor.create is true, provide parentId for the creation of the content item - * @param {String} editor.documentTypeAlias If editor.create is true, provide document type alias for the creation of the content item - * @param {Boolean} editor.allowSaveAndClose If editor is being used in infinite editing allows the editor to close when the save action is performed - * @param {Boolean} editor.allowPublishAndClose If editor is being used in infinite editing allows the editor to close when the publish action is performed - * - * @returns {Object} editor object + * Opens a content editor in infinite editing, the submit callback returns the updated content item. + * + * @param {object} editor rendering options. + * @param {string} editor.id The id of the content item. + * @param {boolean} editor.create Create new content item. + * @param {function} editor.submit Callback function when the publish and close button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @param {string} editor.parentId If editor.create is true, provide parentId for the creation of the content item. + * @param {string} editor.documentTypeAlias If editor.create is true, provide document type alias for the creation of the content item. + * @param {boolean} editor.allowSaveAndClose If editor is being used in infinite editing allows the editor to close when the save action is performed. + * @param {boolean} editor.allowPublishAndClose If editor is being used in infinite editing allows the editor to close when the publish action is performed. + * @returns {object} editor object */ function contentEditor(editor) { editor.view = 'views/content/edit.html'; @@ -2570,15 +4121,15 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a content picker in infinite editing, the submit callback returns an array of selected items + * Opens a content picker in infinite editing, the submit callback returns an array of selected items. * - * @param {Object} editor rendering options - * @param {Boolean} editor.multiPicker Pick one or multiple items - * @param {Int} editor.startNodeId Set the startnode of the picker (optional) - * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object - * @param {Function} editor.close Callback function when the close button is clicked. + * @param {object} editor rendering options. + * @param {boolean} editor.multiPicker Pick one or multiple items. + * @param {number} editor.startNodeId Set the startnode of the picker (optional). + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * - * @returns {Object} editor object + * @returns {object} editor object. */ function contentPicker(editor) { editor.view = 'views/common/infiniteeditors/treepicker/treepicker.html'; @@ -2596,12 +4147,11 @@ When building a custom infinite editor view you can use the same components as a * @description * Opens a content type picker in infinite editing, the submit callback returns an array of selected items * - * @param {Object} editor rendering options - * @param {Boolean} editor.multiPicker Pick one or multiple items - * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object - * @param {Function} editor.close Callback function when the close button is clicked. - * - * @returns {Object} editor object + * @param {object} editor rendering options. + * @param {boolean} editor.multiPicker Pick one or multiple items. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object */ function contentTypePicker(editor) { editor.view = 'views/common/infiniteeditors/treepicker/treepicker.html'; @@ -2617,14 +4167,13 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a media type picker in infinite editing, the submit callback returns an array of selected items - * - * @param {Object} editor rendering options - * @param {Boolean} editor.multiPicker Pick one or multiple items - * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object - * @param {Function} editor.close Callback function when the close button is clicked. + * Opens a media type picker in infinite editing, the submit callback returns an array of selected items. * - * @returns {Object} editor object + * @param {object} editor rendering options. + * @param {boolean} editor.multiPicker Pick one or multiple items. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function mediaTypePicker(editor) { editor.view = 'views/common/infiniteeditors/treepicker/treepicker.html'; @@ -2640,14 +4189,14 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a member type picker in infinite editing, the submit callback returns an array of selected items + * Opens a member type picker in infinite editing, the submit callback returns an array of selected items. * - * @param {Object} editor rendering options - * @param {Boolean} editor.multiPicker Pick one or multiple items - * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object - * @param {Function} editor.close Callback function when the close button is clicked. + * @param {object} editor rendering options. + * @param {boolean} editor.multiPicker Pick one or multiple items. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * - * @returns {Object} editor object + * @returns {object} editor object. */ function memberTypePicker(editor) { editor.view = 'views/common/infiniteeditors/treepicker/treepicker.html'; @@ -2663,12 +4212,14 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a copy editor in infinite editing, the submit callback returns an array of selected items - * @param {String} editor.section The node entity type - * @param {String} editor.currentNode The current node id - * @param {Callback} editor.submit Saves, submits, and closes the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens a copy editor in infinite editing, the submit callback returns an array of selected items. + * + * @param {object} editor rendering options. + * @param {string} editor.section The node entity type. + * @param {string} editor.currentNode The current node id. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function copy(editor) { editor.view = 'views/common/infiniteeditors/copy/copy.html'; @@ -2683,11 +4234,13 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens a move editor in infinite editing. - * @param {String} editor.section The node entity type - * @param {String} editor.currentNode The current node id - * @param {Callback} editor.submit Saves, submits, and closes the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * + * @param {object} editor rendering options. + * @param {string} editor.section The node entity type. + * @param {string} editor.currentNode The current node id. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function move(editor) { editor.view = 'views/common/infiniteeditors/move/move.html'; @@ -2702,9 +4255,11 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens an embed editor in infinite editing. - * @param {Callback} editor.submit Saves, submits, and closes the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function embed(editor) { editor.view = 'views/common/infiniteeditors/embed/embed.html'; @@ -2719,15 +4274,17 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens a rollback editor in infinite editing. - * @param {String} editor.node The node to rollback - * @param {Callback} editor.submit Saves, submits, and closes the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * + * @param {object} editor rendering options. + * @param {string} editor.node The node to rollback. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function rollback(editor) { editor.view = 'views/common/infiniteeditors/rollback/rollback.html'; if (!editor.size) - editor.size = 'small'; + editor.size = 'medium'; open(editor); } /** @@ -2737,12 +4294,13 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens an embed editor in infinite editing. - * @param {Object} editor rendering options - * @param {String} editor.icon The icon class - * @param {String} editor.color The color class - * @param {Callback} editor.submit Saves, submits, and closes the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * + * @param {object} editor rendering options. + * @param {string} editor.icon The icon class. + * @param {string} editor.color The color class. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function linkPicker(editor) { editor.view = 'views/common/infiniteeditors/linkpicker/linkpicker.html'; @@ -2756,13 +4314,14 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a media editor in infinite editing, the submit callback returns the updated media item - * @param {Object} editor rendering options - * @param {String} editor.id The id of the media item - * @param {Boolean} editor.create Create new media item - * @param {Callback} editor.submit Saves, submits, and closes the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens a media editor in infinite editing, the submit callback returns the updated media item. + * + * @param {object} editor rendering options. + * @param {string} editor.id The id of the media item. + * @param {boolean} editor.create Create new media item. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function mediaEditor(editor) { editor.view = 'views/media/edit.html'; @@ -2774,38 +4333,57 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a media picker in infinite editing, the submit callback returns an array of selected media items - * @param {Object} editor rendering options - * @param {Int} editor.startNodeId Set the startnode of the picker (optional) - * @param {Boolean} editor.multiPicker Pick one or multiple items - * @param {Boolean} editor.onlyImages Only display files that have an image file-extension - * @param {Boolean} editor.disableFolderSelect Disable folder selection - * @param {Boolean} editor.disableFocalPoint Disable focal point editor for selected media - * @param {Array} editor.updatedMediaNodes A list of ids for media items that have been updated through the media picker - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens a media picker in infinite editing, the submit callback returns an array of selected media items. + * + * @param {object} editor rendering options. + * @param {number} editor.startNodeId Set the startnode of the picker (optional). + * @param {boolean} editor.multiPicker Pick one or multiple items. + * @param {boolean} editor.onlyImages Only display files that have an image file-extension. + * @param {boolean} editor.disableFolderSelect Disable folder selection. + * @param {boolean} editor.disableFocalPoint Disable focal point editor for selected media. + * @param {array} editor.updatedMediaNodes A list of ids for media items that have been updated through the media picker. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function mediaPicker(editor) { editor.view = 'views/common/infiniteeditors/mediapicker/mediapicker.html'; if (!editor.size) - editor.size = 'small'; + editor.size = 'medium'; editor.updatedMediaNodes = []; open(editor); } /** * @ngdoc method + * @name umbraco.services.editorService#mediaCropDetails + * @methodOf umbraco.services.editorService + * + * @description + * Opens the media crop details editor in infinite editing, the submit callback returns the updated media object. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. + */ + function mediaCropDetails(editor) { + editor.view = 'views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html'; + open(editor); + } + /** + * @ngdoc method * @name umbraco.services.editorService#iconPicker * @methodOf umbraco.services.editorService * * @description - * Opens an icon picker in infinite editing, the submit callback returns the selected icon - * @param {Object} editor rendering options - * @param {String} editor.icon The CSS class representing the icon - eg. "icon-autofill". - * @param {String} editor.color The CSS class representing the color - eg. "color-red". - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens an icon picker in infinite editing, the submit callback returns the selected icon. + * + * @param {object} editor rendering options. + * @param {string} editor.icon The CSS class representing the icon - eg. `icon-autofill. + * @param {string} editor.color The CSS class representing the color - eg. color-red. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function iconPicker(editor) { editor.view = 'views/common/infiniteeditors/iconpicker/iconpicker.html'; @@ -2820,15 +4398,16 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the document type editor in infinite editing, the submit callback returns the alias of the saved document type. - * @param {Object} editor rendering options - * @param {Number} editor.id Indicates the ID of the document type to be edited. Alternatively the ID may be set to `-1` in combination with `create` being set to `true` to open the document type editor for creating a new document type. - * @param {Boolean} editor.create Set to `true` to open the document type editor for creating a new document type. - * @param {Boolean} editor.noTemplate If `true` and in combination with `create` being set to `true`, the document type editor will not create a corresponding template by default. This is similar to selecting the "Document Type without a template" in the Create dialog. - * @param {Boolean} editor.isElement If `true` and in combination with `create` being set to `true`, the "Is an Element type" option will be selected by default in the document type editor. - * @param {Boolean} editor.allowVaryByCulture If `true` and in combination with `create`, the "Allow varying by culture" option will be selected by default in the document type editor. - * @param {Callback} editor.submit Submits the editor. - * @param {Callback} editor.close Closes the editor. - * @returns {Object} editor object + * + * @param {object} editor rendering options. + * @param {number} editor.id Indicates the ID of the document type to be edited. Alternatively the ID may be set to `-1` in combination with `create` being set to `true` to open the document type editor for creating a new document type. + * @param {boolean} editor.create Set to `true` to open the document type editor for creating a new document type. + * @param {boolean} editor.noTemplate If `true` and in combination with `create` being set to `true`, the document type editor will not create a corresponding template by default. This is similar to selecting the "Document Type without a template" in the Create dialog. + * @param {boolean} editor.isElement If `true` and in combination with `create` being set to `true`, the "Is an Element type" option will be selected by default in the document type editor. + * @param {boolean} editor.allowVaryByCulture If `true` and in combination with `create`, the "Allow varying by culture" option will be selected by default in the document type editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function documentTypeEditor(editor) { editor.view = 'views/documenttypes/edit.html'; @@ -2840,11 +4419,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the media type editor in infinite editing, the submit callback returns the saved media type - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens the media type editor in infinite editing, the submit callback returns the saved media type. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function mediaTypeEditor(editor) { editor.view = 'views/mediatypes/edit.html'; @@ -2856,27 +4436,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the member type editor in infinite editing, the submit callback returns the saved member type - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object - */ - function memberTypeEditor(editor) { - editor.view = 'views/membertypes/edit.html'; - open(editor); - } - /** - * @ngdoc method - * @name umbraco.services.editorService#memberTypeEditor - * @methodOf umbraco.services.editorService + * Opens the member type editor in infinite editing, the submit callback returns the saved member type. * - * @description - * Opens the member type editor in infinite editing, the submit callback returns the saved member type - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function memberTypeEditor(editor) { editor.view = 'views/membertypes/edit.html'; @@ -2888,11 +4453,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the query builder in infinite editing, the submit callback returns the generted query - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens the query builder in infinite editing, the submit callback returns the generated query. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function queryBuilder(editor) { editor.view = 'views/common/infiniteeditors/querybuilder/querybuilder.html'; @@ -2904,14 +4470,15 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the query builder in infinite editing, the submit callback returns the generted query - * @param {Object} editor rendering options - * @param {String} options.section tree section to display - * @param {String} options.treeAlias specific tree to display - * @param {Boolean} options.multiPicker should the tree pick one or multiple items before returning - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens the query builder in infinite editing, the submit callback returns the generted query. + * + * @param {object} editor rendering options. + * @param {string} options.section tree section to display. + * @param {string} options.treeAlias specific tree to display. + * @param {boolean} options.multiPicker should the tree pick one or multiple items before returning. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function treePicker(editor) { editor.view = 'views/common/infiniteeditors/treepicker/treepicker.html'; @@ -2926,10 +4493,11 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the an editor to set node permissions. - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function nodePermissions(editor) { editor.view = 'views/common/infiniteeditors/nodepermissions/nodepermissions.html'; @@ -2943,11 +4511,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Open an editor to insert code snippets into the code editor - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Open an editor to insert code snippets into the code editor. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function insertCodeSnippet(editor) { editor.view = 'views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html'; @@ -2961,11 +4530,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the user group picker in infinite editing, the submit callback returns an array of the selected user groups - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens the user group picker in infinite editing, the submit callback returns an array of the selected user groups. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Submits the editor. + * @param {function} editor.close Closes the editor. + * @returns {object} editor object. */ function userGroupPicker(editor) { editor.view = 'views/common/infiniteeditors/usergrouppicker/usergrouppicker.html'; @@ -2975,16 +4545,34 @@ When building a custom infinite editor view you can use the same components as a } /** * @ngdoc method + * @name umbraco.services.editorService#userGroupEditor + * @methodOf umbraco.services.editorService + * + * @description + * Opens the user group picker in infinite editing, the submit callback returns the saved user group. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. + */ + function userGroupEditor(editor) { + editor.view = 'views/users/group.html'; + open(editor); + } + /** + * @ngdoc method * @name umbraco.services.editorService#templateEditor * @methodOf umbraco.services.editorService * * @description - * Opens the user group picker in infinite editing, the submit callback returns the saved template - * @param {Object} editor rendering options - * @param {String} editor.id The template id - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens the template editor in infinite editing, the submit callback returns the saved template. + * + * @param {object} editor rendering options. + * @param {string} editor.id The template id. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function templateEditor(editor) { editor.view = 'views/templates/edit.html'; @@ -2996,11 +4584,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the section picker in infinite editing, the submit callback returns an array of the selected sections¨ - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens the section picker in infinite editing, the submit callback returns an array of the selected sections. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function sectionPicker(editor) { editor.view = 'views/common/infiniteeditors/sectionpicker/sectionpicker.html'; @@ -3014,11 +4603,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the insert field editor in infinite editing, the submit callback returns the code snippet - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens the insert field editor in infinite editing, the submit callback returns the code snippet. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function insertField(editor) { editor.view = 'views/common/infiniteeditors/insertfield/insertfield.html'; @@ -3032,11 +4622,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the template sections editor in infinite editing, the submit callback returns the type to insert - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens the template sections editor in infinite editing, the submit callback returns the type to insert. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function templateSections(editor) { editor.view = 'views/common/infiniteeditors/templatesections/templatesections.html'; @@ -3050,11 +4641,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the section picker in infinite editing, the submit callback returns an array of the selected users - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object + * Opens the section picker in infinite editing, the submit callback returns an array of the selected users. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function userPicker(editor) { editor.view = 'views/common/infiniteeditors/userpicker/userpicker.html'; @@ -3068,15 +4660,15 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the section picker in infinite editing, the submit callback returns an array of the selected items + * Opens the section picker in infinite editing, the submit callback returns an array of the selected items. * - * @param {Object} editor rendering options - * @param {Array} editor.availableItems Array of available items. - * @param {Array} editor.selectedItems Array of selected items. When passed in the selected items will be filtered from the available items. - * @param {Boolean} editor.filter Set to false to hide the filter. - * @param {Callback} editor.submit Submits the editor. - * @param {Callback} editor.close Closes the editor. - * @returns {Object} editor object + * @param {object} editor rendering options. + * @param {array} editor.availableItems Array of available items. + * @param {array} editor.selectedItems Array of selected items. When passed in the selected items will be filtered from the available items. + * @param {boolean} editor.filter Set to false to hide the filter. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function itemPicker(editor) { editor.view = 'views/common/infiniteeditors/itempicker/itempicker.html'; @@ -3090,11 +4682,12 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a macro picker in infinite editing, the submit callback returns an array of the selected items + * Opens a macro picker in infinite editing, the submit callback returns an array of the selected items. * - * @param {Callback} editor.submit Submits the editor. - * @param {Callback} editor.close Closes the editor. - * @returns {Object} editor object + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function macroPicker(editor) { editor.view = 'views/common/infiniteeditors/macropicker/macropicker.html'; @@ -3110,11 +4703,11 @@ When building a custom infinite editor view you can use the same components as a * @description * Opens a member group picker in infinite editing. * - * @param {Object} editor rendering options - * @param {Object} editor.multiPicker Pick one or multiple items. - * @param {Callback} editor.submit Submits the editor. - * @param {Callback} editor.close Closes the editor. - * @returns {Object} editor object + * @param {object} editor rendering options. + * @param {object} editor.multiPicker Pick one or multiple items. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function memberGroupPicker(editor) { editor.view = 'views/common/infiniteeditors/membergrouppicker/membergrouppicker.html'; @@ -3128,14 +4721,13 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a member picker in infinite editing, the submit callback returns an array of selected items + * Opens a member picker in infinite editing, the submit callback returns an array of selected items. * - * @param {Object} editor rendering options - * @param {Boolean} editor.multiPicker Pick one or multiple items - * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object - * @param {Function} editor.close Callback function when the close button is clicked. - * - * @returns {Object} editor object + * @param {object} editor rendering options. + * @param {boolean} editor.multiPicker Pick one or multiple items. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function memberPicker(editor) { editor.view = 'views/common/infiniteeditors/treepicker/treepicker.html'; @@ -3151,15 +4743,15 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens a member editor in infinite editing, the submit callback returns the updated member - * @param {Object} editor rendering options - * @param {String} editor.id The id (GUID) of the member - * @param {Boolean} editor.create Create new member - * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object - * @param {Function} editor.close Callback function when the close button is clicked. - * @param {String} editor.doctype If editor.create is true, provide member type for the creation of the member - * - * @returns {Object} editor object + * Opens a member editor in infinite editing, the submit callback returns the updated member. + * + * @param {object} editor rendering options. + * @param {string} editor.id The id (GUID) of the member. + * @param {boolean} editor.create Create new member. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @param {string} editor.doctype If `editor.create` is `true`, provide member type for the creation of the member. + * @returns {object} editor object. */ function memberEditor(editor) { editor.view = 'views/member/edit.html'; @@ -3173,11 +4765,10 @@ When building a custom infinite editor view you can use the same components as a * * @description * Internal method to keep track of keyboard shortcuts registered - * to each editor so they can be rebound when an editor closes - * + * to each editor so they can be rebound when an editor closes. */ function unbindKeyboardShortcuts() { - var shortcuts = angular.copy(keyboardService.keyboardEvent); + var shortcuts = Utilities.copy(keyboardService.keyboardEvent); editorsKeyboardShorcuts.push(shortcuts); // unbind the current shortcuts because we only want to // shortcuts from the newly opened editor working @@ -3192,8 +4783,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Internal method to rebind keyboard shortcuts for the editor in focus - * + * Internal method to rebind keyboard shortcuts for the editor in focus. */ function rebindKeyboardShortcuts() { // find the shortcuts from the previous editor @@ -3234,6 +4824,7 @@ When building a custom infinite editor view you can use the same components as a nodePermissions: nodePermissions, insertCodeSnippet: insertCodeSnippet, userGroupPicker: userGroupPicker, + userGroupEditor: userGroupEditor, templateEditor: templateEditor, sectionPicker: sectionPicker, insertField: insertField, @@ -3243,7 +4834,8 @@ When building a custom infinite editor view you can use the same components as a macroPicker: macroPicker, memberGroupPicker: memberGroupPicker, memberPicker: memberPicker, - memberEditor: memberEditor + memberEditor: memberEditor, + mediaCropDetails: mediaCropDetails }; return service; } @@ -3352,7 +4944,7 @@ When building a custom infinite editor view you can use the same components as a 'use strict'; /** Used to broadcast and listen for global events and allow the ability to add async listeners to the callbacks */ /* - Core app events: + Core app events: app.ready app.authenticated @@ -3378,7 +4970,7 @@ When building a custom infinite editor view you can use the same components as a }, /** pass in the result of 'on' to this method, or just call the method returned from 'on' to unsubscribe */ unsubscribe: function unsubscribe(handle) { - if (angular.isFunction(handle)) { + if (Utilities.isFunction(handle)) { handle(); } } @@ -3388,6 +4980,70 @@ When building a custom infinite editor view you can use the same components as a 'use strict'; /** * @ngdoc service + * @name umbraco.services.externalLoginInfoService + * @description A service for working with external login providers + **/ + function externalLoginInfoService(externalLoginInfo, umbRequestHelper) { + function getLoginProvider(provider) { + if (provider) { + var found = _.find(externalLoginInfo.providers, function (x) { + return x.authType == provider; + }); + return found; + } + return null; + } + function getLoginProviderView(provider) { + if (provider && provider.properties && provider.properties.UmbracoBackOfficeExternalLoginOptions && provider.properties.UmbracoBackOfficeExternalLoginOptions.CustomBackOfficeView) { + return umbRequestHelper.convertVirtualToAbsolutePath(provider.properties.UmbracoBackOfficeExternalLoginOptions.CustomBackOfficeView); + } + return null; + } + /** + * Returns true if any provider denies local login if `provider` is null, else whether the passed + * @param {any} provider + */ + function hasDenyLocalLogin(provider) { + if (!provider) { + return _.some(externalLoginInfo.providers, function (x) { + return x.properties && x.properties.UmbracoBackOfficeExternalLoginOptions && x.properties.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin === true; + }); + } else { + return provider && provider.properties && provider.properties.UmbracoBackOfficeExternalLoginOptions && provider.properties.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin === true; + } + } + /** + * Returns all login providers + */ + function getLoginProviders() { + return externalLoginInfo.providers; + } + /** Returns all logins providers that have options that the user can interact with */ + function getLoginProvidersWithOptions() { + // only include providers that allow manual linking or ones that provide a custom view + var providers = _.filter(externalLoginInfo.providers, function (x) { + // transform the data and also include the custom view as a nicer property + x.customView = getLoginProviderView(x); + if (x.customView) { + return true; + } else { + return x.properties.ExternalSignInAutoLinkOptions.AllowManualLinking; + } + }); + return providers; + } + return { + hasDenyLocalLogin: hasDenyLocalLogin, + getLoginProvider: getLoginProvider, + getLoginProviders: getLoginProviders, + getLoginProvidersWithOptions: getLoginProvidersWithOptions, + getLoginProviderView: getLoginProviderView + }; + } + angular.module('umbraco.services').factory('externalLoginInfoService', externalLoginInfoService); + 'use strict'; + /** + * @ngdoc service * @name umbraco.services.fileManager * @function * @@ -3411,23 +5067,26 @@ When building a custom infinite editor view you can use the same components as a */ setFiles: function setFiles(args) { //propertyAlias, files - if (!angular.isString(args.propertyAlias)) { + if (!Utilities.isString(args.propertyAlias)) { throw 'args.propertyAlias must be a non empty string'; } - if (!angular.isObject(args.files)) { + if (!Utilities.isObject(args.files)) { throw 'args.files must be an object'; } //normalize to null if (!args.culture) { args.culture = null; } + if (!args.segment) { + args.segment = null; + } var metaData = []; - if (angular.isArray(args.metaData)) { + if (Utilities.isArray(args.metaData)) { metaData = args.metaData; } - //this will clear the files for the current property/culture and then add the new ones for the current property + //this will clear the files for the current property/culture/segment and then add the new ones for the current property fileCollection = _.reject(fileCollection, function (item) { - return item.alias === args.propertyAlias && (!args.culture || args.culture === item.culture); + return item.alias === args.propertyAlias && (!args.culture || args.culture === item.culture) && (!args.segment || args.segment === item.segment); }); for (var i = 0; i < args.files.length; i++) { //save the file object to the files collection @@ -3435,6 +5094,7 @@ When building a custom infinite editor view you can use the same components as a alias: args.propertyAlias, file: args.files[i], culture: args.culture, + segment: args.segment, metaData: metaData }); } @@ -3539,6 +5199,47 @@ When building a custom infinite editor view you can use the same components as a angular.module('umbraco.services').factory('focusService', focusService); }()); 'use strict'; + (function () { + 'use strict'; + function focusLockService($document) { + var elementToInert = $document[0].querySelector('#mainwrapper'); + function addInertAttribute() { + if (elementToInert) { + elementToInert.setAttribute('inert', true); + } + } + function removeInertAttribute() { + if (elementToInert) { + elementToInert.removeAttribute('inert'); + } + } + var service = { + addInertAttribute: addInertAttribute, + removeInertAttribute: removeInertAttribute + }; + return service; + } + angular.module('umbraco.services').factory('focusLockService', focusLockService); + }()); + 'use strict'; + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); + } + function _nonIterableSpread() { + throw new TypeError('Invalid attempt to spread non-iterable instance'); + } + function _iterableToArray(iter) { + if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === '[object Arguments]') + return Array.from(iter); + } + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } + } /** * @ngdoc service * @name umbraco.services.formHelper @@ -3557,10 +5258,10 @@ When building a custom infinite editor view you can use the same components as a * @function * * @description - * Called by controllers when submitting a form - this ensures that all client validation is checked, + * Called by controllers when submitting a form - this ensures that all client validation is checked, * server validation is cleared, that the correct events execute and status messages are displayed. * 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 submitForm(args) { @@ -3582,17 +5283,55 @@ When building a custom infinite editor view you can use the same components as a scope: args.scope, action: args.action }); + this.focusOnFirstError(currentForm); + // Some property editors need to perform an action after all property editors have reacted to the formSubmitting. + args.scope.$broadcast('formSubmittingFinalPhase', { + scope: args.scope, + action: args.action + }); + // Set the form state to submitted + currentForm.$setSubmitted(); //then check if the form is valid if (!args.skipValidation) { if (currentForm.$invalid) { return false; } } - //reset the server validations - serverValidationManager.reset(); + //reset the server validations if required (default is true), otherwise notify existing ones of changes + if (!args.keepServerValidation) { + serverValidationManager.reset(); + } else { + serverValidationManager.notify(); + } return true; }, /** + * @ngdoc function + * @name umbraco.services.formHelper#focusOnFirstError + * @methodOf umbraco.services.formHelper + * @function + * + * @description + * Called by submitForm when a form has been submitted, it will fire a focus on the first found invalid umb-property it finds in the form.. + * + * @param {object} form Pass in a form object. + */ + focusOnFirstError: function focusOnFirstError(form) { + var invalidNgForms = form.$$element.find('.umb-property ng-form.ng-invalid, .umb-property-editor ng-form.ng-invalid-required'); + var firstInvalidNgForm = invalidNgForms.first(); + if (firstInvalidNgForm.length !== 0) { + var focusableFields = _toConsumableArray(firstInvalidNgForm.find('umb-range-slider .noUi-handle,input,textarea,select,button')); + if (focusableFields.length !== 0) { + var firstErrorEl = focusableFields.find(function (el) { + return el.type !== 'hidden' && el.hasAttribute('readonly') === false; + }); + if (firstErrorEl !== undefined) { + firstErrorEl.focus(); + } + } + } + }, + /** * @ngdoc function * @name umbraco.services.formHelper#submitForm * @methodOf umbraco.services.formHelper @@ -3600,23 +5339,33 @@ When building a custom infinite editor view you can use the same components as a * * @description * Called by controllers when a form has been successfully submitted, this ensures the correct events are raised. - * + * * @param {object} args An object containing arguments for form submission */ resetForm: function resetForm(args) { + var currentForm; if (!args) { throw 'args cannot be null'; } if (!args.scope) { throw 'args.scope cannot be null'; } - args.scope.$broadcast('formSubmitted', { scope: args.scope }); + if (!args.formCtrl) { + //try to get the closest form controller + currentForm = angularHelper.getRequiredCurrentForm(args.scope); + } else { + currentForm = args.formCtrl; + } + // Set the form state to pristine + currentForm.$setPristine(); + currentForm.$setUntouched(); + args.scope.$broadcast(args.hasErrors ? 'formSubmittedValidationFailed' : 'formSubmitted', { scope: args.scope }); }, showNotifications: function showNotifications(args) { if (!args || !args.notifications) { return false; } - if (angular.isArray(args.notifications)) { + if (Utilities.isArray(args.notifications)) { for (var i = 0; i < args.notifications.length; i++) { notificationsService.showNotification(args.notifications[i]); } @@ -3633,7 +5382,7 @@ When building a custom infinite editor view you can use the same components as a * @description * Needs to be called when a form submission fails, this will wire up all server validation errors in ModelState and * 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 handleError(err) { @@ -3664,52 +5413,11 @@ When building a custom infinite editor view you can use the same components as a * * @description * 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 handleServerValidation(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 there's some special ModelState syntax we need to know about. - // 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 for a content 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 3 parts for content 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]; - var culture = null; - if (parts.length > 2) { - culture = parts[2]; - //special check in case the string is formatted this way - if (culture === 'null') { - culture = null; - } - } - //if it contains 3 '.' then we will wire it up to a property's html field - if (parts.length > 3) { - //add an error with a reference to the field for which the validation belongs too - serverValidationManager.addPropertyError(propertyAlias, culture, parts[3], modelState[e][0]); - } else { - //add a generic error for the property, no reference to a specific html field - serverValidationManager.addPropertyError(propertyAlias, culture, '', 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]); - } - } + serverValidationManager.addErrorsForModelState(modelState); } }; } @@ -3932,12 +5640,50 @@ When building a custom infinite editor view you can use the same components as a }; }); 'use strict'; + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); + } + function _nonIterableRest() { + throw new TypeError('Invalid attempt to destructure non-iterable instance'); + } + function _iterableToArrayLimit(arr, i) { + if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === '[object Arguments]')) { + return; + } + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + if (i && _arr.length === i) + break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i['return'] != null) + _i['return'](); + } finally { + if (_d) + throw _e; + } + } + return _arr; + } + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) + return arr; + } /** * @ngdoc service * @name umbraco.services.iconHelper * @description A helper service for dealing with icons, mostly dealing with legacy tree icons **/ - function iconHelper($q, $timeout) { + function iconHelper($http, $q, $sce, $timeout) { var converter = [ { oldIcon: '.sprNew', @@ -4120,8 +5866,7 @@ When building a custom infinite editor view you can use the same components as a { oldIcon: ".sprTreeSettingAgent", newIcon: "" }, { oldIcon: ".sprTreeSettingCss", newIcon: "" }, { oldIcon: ".sprTreeSettingCssItem", newIcon: "" }, - - { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" }, + { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" }, { oldIcon: ".sprTreeSettingDomain", newIcon: "" }, { oldIcon: ".sprTreeSettingLanguage", newIcon: "" }, { oldIcon: ".sprTreeSettingScript", newIcon: "" }, @@ -4165,11 +5910,57 @@ When building a custom infinite editor view you can use the same components as a newIcon: 'icon-linux' } ]; + var collectedIcons; var imageConverter = [{ oldImage: 'contour.png', newIcon: 'icon-umb-contour' }]; - var collectedIcons; + var iconCache = []; + var promiseQueue = []; + var resourceLoadStatus = 'none'; + /** + * This is the same approach as use for loading the localized text json + * We don't want multiple requests for the icon collection, so need to track + * the current request state, and resolve the queued requests once the icons arrive + * Subsequent requests are returned immediately as the icons are cached into + */ + function init() { + var deferred = $q.defer(); + if (resourceLoadStatus === 'loaded') { + deferred.resolve(iconCache); + return deferred.promise; + } + if (resourceLoadStatus === 'loading') { + promiseQueue.push(deferred); + return deferred.promise; + } + resourceLoadStatus = 'loading'; + $http({ + method: 'GET', + url: Umbraco.Sys.ServerVariables.umbracoUrls.iconApiBaseUrl + 'GetIcons' + }).then(function (response) { + resourceLoadStatus = 'loaded'; + for (var _i = 0, _Object$entries = Object.entries(response.data.Data); _i < _Object$entries.length; _i++) { + var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2), key = _Object$entries$_i[0], value = _Object$entries$_i[1]; + iconCache.push({ + name: key, + svgString: $sce.trustAsHtml(value) + }); + } + deferred.resolve(iconCache); + //ensure all other queued promises are resolved + for (var p in promiseQueue) { + promiseQueue[p].resolve(iconCache); + } + }, function (err) { + deferred.reject('Something broke'); + //ensure all other queued promises are resolved + for (var p in promiseQueue) { + promiseQueue[p].reject('Something broke'); + } + }); + return deferred.promise; + } return { /** Used by the create dialogs for content/media types to format the data so that the thumbnails are styled properly */ formatContentTypeThumbnails: function formatContentTypeThumbnails(contentTypes) { @@ -4227,7 +6018,45 @@ When building a custom infinite editor view you can use the same components as a } return false; }, - /** Return a list of icons, optionally filter them */ + /** Converts the icon from legacy to a new one if an old one is detected */ + convertFromLegacyIcon: function convertFromLegacyIcon(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 icon; + }, + convertFromLegacyImage: function convertFromLegacyImage(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 convertFromLegacyTreeNodeIcon(treeNode) { + if (this.isLegacyTreeNodeIcon(treeNode)) { + return this.convertFromLegacyIcon(treeNode.icon); + } + return treeNode.icon; + }, + /** Gets a single IconModel */ + getIcon: function getIcon(iconName) { + return init().then(function (icons) { + return icons.find(function (i) { + return i.name === iconName; + }); + }); + }, + /** Gets all the available icons in the backoffice icon folder and returns them as an array of IconModels */ + getAllIcons: function getAllIcons() { + return init().then(function (icons) { + return icons; + }); + }, + /** LEGACY - Return a list of icons from icon fonts, optionally filter them */ /** It fetches them directly from the active stylesheets in the browser */ getIcons: function getIcons() { var deferred = $q.defer(); @@ -4270,29 +6099,25 @@ When building a custom infinite editor view you can use the same components as a }, 100); return deferred.promise; }, - /** Converts the icon from legacy to a new one if an old one is detected */ - convertFromLegacyIcon: function convertFromLegacyIcon(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; + /** Creates a icon object, and caches it in a runtime cache */ + defineIcon: function defineIcon(name, svg) { + var icon = iconCache.find(function (x) { + return x.name === name; + }); + if (icon === undefined) { + icon = { + name: name, + svgString: $sce.trustAsHtml(svg) + }; + iconCache.push(icon); } return icon; }, - convertFromLegacyImage: function convertFromLegacyImage(icon) { - var found = _.find(imageConverter, function (item) { - return item.oldImage.toLowerCase() === icon.toLowerCase(); + /** Returns the cached icon or undefined */ + _getIconFromCache: function _getIconFromCache(iconName) { + return iconCache.find(function (icon) { + return icon.name === iconName; }); - 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 convertFromLegacyTreeNodeIcon(treeNode) { - if (this.isLegacyTreeNodeIcon(treeNode)) { - return this.convertFromLegacyIcon(treeNode.icon); - } - return treeNode.icon; } }; } @@ -4593,7 +6418,7 @@ When building a custom infinite editor view you can use the same components as a } var elt; // Initialize opt object - opt = angular.extend({}, defaultOpt, opt); + opt = Utilities.extend({}, defaultOpt, opt); label = label.toLowerCase(); elt = opt.target; if (typeof opt.target === 'string') { @@ -4694,7 +6519,7 @@ When building a custom infinite editor view you can use the same components as a */ (function () { 'use strict'; - function listViewHelper($location, localStorageService, urlHelper) { + function listViewHelper($location, $rootScope, localStorageService, urlHelper, editorService) { var firstSelectedIndex = 0; var localStorageKey = 'umblistViewLayout'; /** @@ -4885,6 +6710,7 @@ When building a custom infinite editor view you can use the same components as a } selection.push(obj); item.selected = true; + $rootScope.$broadcast('listView.itemsChanged', { items: selection }); } } /** @@ -4905,6 +6731,7 @@ When building a custom infinite editor view you can use the same components as a if (item.id !== 2147483647 && item.id === selectedItem.id || item.key && item.key === selectedItem.key) { selection.splice(i, 1); item.selected = false; + $rootScope.$broadcast('listView.itemsChanged', { items: selection }); } } } @@ -4924,18 +6751,19 @@ When building a custom infinite editor view you can use the same components as a function clearSelection(items, folders, selection) { var i = 0; selection.length = 0; - if (angular.isArray(items)) { + if (Utilities.isArray(items)) { for (i = 0; items.length > i; i++) { var item = items[i]; item.selected = false; } } - if (angular.isArray(folders)) { + if (Utilities.isArray(folders)) { for (i = 0; folders.length > i; i++) { var folder = folders[i]; folder.selected = false; } } + $rootScope.$broadcast('listView.itemsChanged', { items: selection }); } /** * @ngdoc method @@ -4953,7 +6781,7 @@ When building a custom infinite editor view you can use the same components as a function selectAllItems(items, selection, $event) { var checkbox = $event.target; var clearSelection = false; - if (!angular.isArray(items)) { + if (!Utilities.isArray(items)) { return; } selection.length = 0; @@ -4973,6 +6801,7 @@ When building a custom infinite editor view you can use the same components as a if (clearSelection) { selection.length = 0; } + $rootScope.$broadcast('listView.itemsChanged', { items: selection }); } /** * @ngdoc method @@ -4986,12 +6815,12 @@ When building a custom infinite editor view you can use the same components as a * @param {Array} selection Listview selection, available as $scope.selection */ function selectAllItemsToggle(items, selection) { - if (!angular.isArray(items)) { + if (!Utilities.isArray(items)) { return; } if (isSelectedAll(items, selection)) { // unselect all items - angular.forEach(items, function (item) { + items.forEach(function (item) { item.selected = false; }); // reset selection without loosing reference. @@ -5000,7 +6829,7 @@ When building a custom infinite editor view you can use the same components as a // reset selection without loosing reference. selection.length = 0; // select all items - angular.forEach(items, function (item) { + items.forEach(function (item) { var obj = { id: item.id }; if (item.key) { obj.key = item.key; @@ -5009,6 +6838,7 @@ When building a custom infinite editor view you can use the same components as a selection.push(obj); }); } + $rootScope.$broadcast('listView.itemsChanged', { items: selection }); } /** * @ngdoc method @@ -5117,11 +6947,38 @@ When building a custom infinite editor view you can use the same components as a * Method for opening an item in a list view for editing. * * @param {Object} item The item to edit + * @param {Object} scope The scope with options */ - function editItem(item) { + function editItem(item, scope) { if (!item.editPath) { return; } + if (scope && scope.options && scope.options.useInfiniteEditor) { + var editorModel = { + id: item.id, + submit: function submit(model) { + editorService.close(); + scope.getContent(scope.contentId); + }, + close: function close() { + editorService.close(); + scope.getContent(scope.contentId); + } + }; + if (item.editPath.indexOf('/content/') == 0) { + editorService.contentEditor(editorModel); + return; + } + if (item.editPath.indexOf('/media/') == 0) { + editorService.mediaEditor(editorModel); + return; + } + if (item.editPath.indexOf('/member/') == 0) { + editorModel.id = item.key; + editorService.memberEditor(editorModel); + return; + } + } var parts = item.editPath.split('?'); var path = parts[0]; var params = parts[1] ? urlHelper.getQueryStringParams('?' + parts[1]) : {}; @@ -5223,7 +7080,7 @@ When building a custom infinite editor view you can use the same components as a * }); * */ - angular.module('umbraco.services').factory('localizationService', function ($http, $q, eventsService, $window, $filter, userService) { + angular.module('umbraco.services').factory('localizationService', function ($http, $q, eventsService) { // TODO: This should be injected as server vars var url = 'LocalizedText'; var resourceFileLoadStatus = 'none'; @@ -5257,6 +7114,7 @@ When building a custom infinite editor view you can use the same components as a var service = { // loads the language resource file from the server initLocalizedResources: function initLocalizedResources() { + // TODO: This promise handling is super ugly, we should just be returnning the promise from $http and returning inner values. var deferred = $q.defer(); if (resourceFileLoadStatus === 'loaded') { deferred.resolve(innerDictionary); @@ -5302,7 +7160,7 @@ When building a custom infinite editor view you can use the same components as a * @description * Helper to tokenize and compile a localization string * @param {String} value the value to tokenize - * @param {Object} scope the $scope object + * @param {Object} scope the $scope object * @returns {String} tokenized resource string */ tokenize: function tokenize(value, scope) { @@ -5330,13 +7188,13 @@ When building a custom infinite editor view you can use the same components as a * @description * Helper to replace tokens * @param {String} value the text-string to manipulate - * @param {Array} tekens An array of tokens values + * @param {Array} tekens An array of tokens values * @returns {String} Replaced test-string */ tokenReplace: function tokenReplace(text, tokens) { if (tokens) { for (var i = 0; i < tokens.length; i++) { - text = text.replace('%' + i + '%', tokens[i]); + text = text.replace('%' + i + '%', _.escape(tokens[i])); } } return text; @@ -5348,16 +7206,16 @@ When building a custom infinite editor view you can use the same components as a * * @description * Checks the dictionary for a localized resource string - * @param {String} value the area/key to localize in the format of 'section_key' + * @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 - * - * @param {String} fallbackValue if specified this string will be returned if no matching + * + * @param {String} fallbackValue if specified this string will be returned if no matching * entry was found in the dictionary - * + * * @returns {String} localized resource string */ localize: function localize(value, tokens, fallbackValue) { @@ -5373,7 +7231,7 @@ When building a custom infinite editor view you can use the same components as a * @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){
      @@ -5382,11 +7240,11 @@ When building a custom infinite editor view you can use the same components as a
            *      notificationService.error(header, message);
            * });
            * 
      - * - * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key' + * + * @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 localizeMany(keys) { @@ -5409,18 +7267,18 @@ When building a custom infinite editor view you can use the same components as a * @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' + * + * @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 concat(keys) { @@ -5448,7 +7306,7 @@ When building a custom infinite editor view you can use the same components as a * @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){
      @@ -5456,14 +7314,14 @@ When building a custom infinite editor view you can use the same components as a
            *      var formattedResult = data;
            * });
            * 
      - * - * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key' + * + * @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 format(keys, message) { @@ -5547,11 +7405,11 @@ When building a custom infinite editor view you can use the same components as a val = val ? val : ''; //need to detect if the val is a string or an object var keyVal; - if (angular.isString(val)) { + if (Utilities.isString(val)) { keyVal = key + '="' + (val ? val : '') + '" '; } else { //if it's not a string we'll send it through the json serializer - var json = angular.toJson(val); + var json = Utilities.toJson(val); //then we need to url encode it so that it's safe var encoded = encodeURIComponent(json); keyVal = key + '="' + encoded + '" '; @@ -5605,7 +7463,7 @@ When building a custom infinite editor view you can use the same components as a var val = item.value; if (item.value !== null && item.value !== undefined && !_.isString(item.value)) { try { - val = angular.toJson(val); + val = Utilities.toJson(val); } catch (e) { } } @@ -5677,7 +7535,7 @@ When building a custom infinite editor view you can use the same components as a } //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) { + if (Utilities.isString(item.value) && item.value.indexOf(mediaRoot) === 0) { return true; } return false; @@ -5763,7 +7621,7 @@ When building a custom infinite editor view you can use the same components as a * @param {boolean} thumbnail Whether to return the thumbnail url or normal url */ resolveFileFromEntity: function resolveFileFromEntity(mediaEntity, thumbnail) { - var mediaPath = angular.isObject(mediaEntity.metaData) ? mediaEntity.metaData.MediaPath : null; + var mediaPath = Utilities.isObject(mediaEntity.metaData) ? mediaEntity.metaData.MediaPath : null; if (!mediaPath) { //don't throw since this image legitimately might not contain a media path, but output a warning $log.warn('Cannot resolve the file url from the mediaEntity, it does not contain the required metaData'); @@ -6015,9 +7873,9 @@ When building a custom infinite editor view you can use the same components as a }, getAllowedImagetypes: function getAllowedImagetypes(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 + //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 + //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) { @@ -6026,17 +7884,11 @@ When building a custom infinite editor view you can use the same components as a }); // 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' - ]); - } + // Find all the media types with an Image Cropper or Upload Field property editor + return mediaTypeHelperService.getTypeWithEditor(fullTypes, [ + 'Umbraco.ImageCropper', + 'Umbraco.UploadField' + ]); }); }); }, @@ -6052,6 +7904,36 @@ When building a custom infinite editor view you can use the same components as a } } }); + }, + getTypeAcceptingFileExtensions: function getTypeAcceptingFileExtensions(mediaTypes, fileExtensions) { + return mediaTypes.filter(function (mediaType) { + var uploadProperty; + mediaType.groups.forEach(function (group) { + var foundProperty = group.properties.find(function (property) { + return property.alias === 'umbracoFile'; + }); + if (foundProperty) { + uploadProperty = foundProperty; + } + }); + if (uploadProperty) { + var acceptedFileExtensions; + if (uploadProperty.editor === 'Umbraco.ImageCropper') { + acceptedFileExtensions = Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes; + } else if (uploadProperty.editor === 'Umbraco.UploadField') { + acceptedFileExtensions = uploadProperty.config.fileExtensions && uploadProperty.config.fileExtensions.length > 0 ? uploadProperty.config.fileExtensions.map(function (x) { + return x.value; + }) : null; + } + if (acceptedFileExtensions && acceptedFileExtensions.length > 0) { + return fileExtensions.length === fileExtensions.filter(function (fileExt) { + return acceptedFileExtensions.includes(fileExt); + }).length; + } + return true; + } + return false; + }); } }; return mediaTypeHelperService; @@ -6173,7 +8055,7 @@ When building a custom infinite editor view you can use the same components as a * Section navigation and search, and maintain their state for the entire application lifetime * */ - function navigationService($routeParams, $location, $q, $injector, eventsService, umbModelMapper, treeService, appState) { + function navigationService($routeParams, $location, $q, $injector, eventsService, umbModelMapper, treeService, appState, backdropService) { //the promise that will be resolved when the navigation is ready var navReadyPromise = $q.defer(); //the main tree's API reference, this is acquired when the tree has initialized @@ -6182,10 +8064,15 @@ When building a custom infinite editor view you can use the same components as a mainTreeApi = args.treeApi; navReadyPromise.resolve(mainTreeApi); }); + eventsService.on('appState.backdrop', function (e, args) { + var element = $(args.element); + element.addClass('above-backdrop'); + }); //A list of query strings defined that when changed will not cause a reload of the route var nonRoutingQueryStrings = [ 'mculture', 'cculture', + 'csegment', 'lq', 'sr' ]; @@ -6243,7 +8130,7 @@ When building a custom infinite editor view you can use the same components as a * @param {any} requestPath */ function pathToRouteParts(requestPath) { - if (!angular.isString(requestPath)) { + if (!Utilities.isString(requestPath)) { throw 'The value for requestPath is not a string'; } var pathAndQuery = requestPath.split('#')[1]; @@ -6265,6 +8152,25 @@ When building a custom infinite editor view you can use the same components as a return result; } } + function closeBackdrop() { + var tourIsOpen = document.body.classList.contains('umb-tour-is-visible'); + if (tourIsOpen) { + return; + } + var aboveClass = 'above-backdrop'; + var leftColumn = document.getElementById('leftcolumn'); + if (leftColumn) { + var isLeftColumnOnTop = leftColumn.classList.contains(aboveClass); + if (isLeftColumnOnTop) { + backdropService.close(); + leftColumn.classList.remove(aboveClass); + } + } + } + function showBackdrop() { + var backDropOptions = { 'element': document.getElementById('leftcolumn') }; + backdropService.open(backDropOptions); + } var service = { /** * @ngdoc method @@ -6280,10 +8186,10 @@ When building a custom infinite editor view you can use the same components as a * @param {object} nextUrlParams Either a string path or a dictionary of route parameters */ isRouteChangingNavigation: function isRouteChangingNavigation(currUrlParams, nextUrlParams) { - if (angular.isString(currUrlParams)) { + if (Utilities.isString(currUrlParams)) { currUrlParams = pathToRouteParts(currUrlParams); } - if (angular.isString(nextUrlParams)) { + if (Utilities.isString(nextUrlParams)) { nextUrlParams = pathToRouteParts(nextUrlParams); } //check if there is a query string to indicate that a "soft redirect" is taking place, if so we are not changing navigation @@ -6300,7 +8206,7 @@ When building a custom infinite editor view you can use the same components as a //if the routing parameter keys are the same, we'll compare their values to see if any have changed and if so then the routing will be allowed. if (diff1.length === 0 && diff2.length === 0) { var partsChanged = 0; - _.each(currRoutingKeys, function (k) { + currRoutingKeys.forEach(function (k) { if (currUrlParams[k] != nextUrlParams[k]) { partsChanged++; } @@ -6334,7 +8240,7 @@ When building a custom infinite editor view you can use the same components as a var toRetain = _.union(retainedQueryStrings, toRetain); var currentSearch = $location.search(); $location.search(''); - _.each(toRetain, function (k) { + toRetain.forEach(function (k) { if (currentSearch[k]) { $location.search(k, currentSearch[k]); } @@ -6363,9 +8269,9 @@ When building a custom infinite editor view you can use the same components as a * @param {Object} nextRouteParams The next route parameters */ retainQueryStrings: function retainQueryStrings(currRouteParams, nextRouteParams) { - var toRetain = angular.copy(nextRouteParams); + var toRetain = Utilities.copy(nextRouteParams); var updated = false; - _.each(retainedQueryStrings, function (r) { + retainedQueryStrings.forEach(function (r) { // if mculture is set to null in nextRouteParams, the value will be undefined and we will not retain any query string that has a value of "null" if (currRouteParams[r] && nextRouteParams[r] !== undefined && !nextRouteParams[r]) { toRetain[r] = currRouteParams[r]; @@ -6433,7 +8339,7 @@ When building a custom infinite editor view you can use the same components as a hideTray: function hideTray() { appState.setGlobalState('showTray', false); }, - /** + /** * @ngdoc method * @name umbraco.services.navigationService#syncTree * @methodOf umbraco.services.navigationService @@ -6464,14 +8370,14 @@ When building a custom infinite editor view you can use the same components as a return mainTreeApi.syncTree(args); }); }, - /** + /** * @ngdoc method * @name umbraco.services.navigationService#hasTree * @methodOf umbraco.services.navigationService * * @description * Checks if a tree with the given alias exists. - * + * * @param {String} treeAlias the tree alias to check */ hasTree: function hasTree(treeAlias) { @@ -6532,6 +8438,7 @@ When building a custom infinite editor view you can use the same components as a showMenu: function showMenu(args) { var self = this; return treeService.getMenu({ treeNode: args.node }).then(function (data) { + showBackdrop(); //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. @@ -6571,6 +8478,7 @@ When building a custom infinite editor view you can use the same components as a appState.setMenuState('currentNode', null); appState.setMenuState('menuActions', []); setMode('tree'); + closeBackdrop(); }, /** Executes a given menu action */ executeMenuAction: function executeMenuAction(action, node, section) { @@ -6584,13 +8492,13 @@ When building a custom infinite editor view you can use the same components as a throw 'section cannot be null'; } appState.setMenuState('currentNode', node); - if (action.metaData && action.metaData['actionRoute'] && angular.isString(action.metaData['actionRoute'])) { + if (action.metaData && action.metaData['actionRoute'] && Utilities.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'])) { + } else if (action.metaData && action.metaData['jsAction'] && Utilities.isString(action.metaData['jsAction'])) { //we'll try to get the jsAction from the injector var menuAction = action.metaData['jsAction'].split('.'); if (menuAction.length !== 2) { @@ -6618,6 +8526,7 @@ When building a custom infinite editor view you can use the same components as a }]); } } else { + showBackdrop(); service.showDialog({ node: node, action: action, @@ -6664,20 +8573,11 @@ When building a custom infinite editor view you can use the same components as a if (args.action.metaData['actionView']) { templateUrl = args.action.metaData['actionView']; } 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'; - } + templateUrl = this.getTreeTemplateUrl(treeAlias, args.action.alias); } setMode('dialog'); if (templateUrl) { @@ -6686,6 +8586,28 @@ When building a custom infinite editor view you can use the same components as a }, /** * @ngdoc method + * @name umbraco.services.navigationService#getTreeTemplateUrl + * @methodOf umbraco.services.navigationService + * + * @param {string} treeAlias the alias of the tree to look up + * @param {string} action the view file name + * @description + * creates the templateUrl based on treeAlias and action + * 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 + */ + getTreeTemplateUrl: function getTreeTemplateUrl(treeAlias, action) { + var packageTreeFolder = treeService.getTreePackageFolder(treeAlias); + if (packageTreeFolder) { + return Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + '/' + packageTreeFolder + '/backoffice/' + treeAlias + '/' + action + '.html'; + } else { + return 'views/' + treeAlias + '/' + action + '.html'; + } + }, + /** + * @ngdoc method * @name umbraco.services.navigationService#allowHideDialog * @methodOf umbraco.services.navigationService * @@ -6717,6 +8639,7 @@ When building a custom infinite editor view you can use the same components as a node: appState.getMenuState('currentNode') }); } else { + closeBackdrop(); setMode('default'); } }, @@ -6752,6 +8675,7 @@ When building a custom infinite editor view you can use the same components as a */ hideNavigation: function hideNavigation() { appState.setMenuState('menuActions', []); + closeBackdrop(); setMode('default'); } }; @@ -6806,6 +8730,7 @@ When building a custom infinite editor view you can use the same components as a * @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.target the target used together with `url`. Empty if not specified. * @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 @@ -7004,7 +8929,7 @@ When building a custom infinite editor view you can use the same components as a * @param {Int} index index where the notication should be removed from */ remove: function remove(index) { - if (angular.isObject(index)) { + if (Utilities.isObject(index)) { var i = nArray.indexOf(index); angularHelper.safeApply($rootScope, function () { nArray.splice(i, 1); @@ -7063,7 +8988,7 @@ When building a custom infinite editor view you can use the same components as a */ (function () { 'use strict'; - function overlayService(eventsService, backdropService) { + function overlayService(eventsService, backdropService, focusLockService) { var currentOverlay = null; function open(newOverlay) { // prevent two open overlays at the same time @@ -7089,12 +9014,17 @@ When building a custom infinite editor view you can use the same components as a backdropOptions.disableEventsOnClick = true; } overlay.show = true; + focusLockService.addInertAttribute(); backdropService.open(backdropOptions); currentOverlay = overlay; eventsService.emit('appState.overlay', overlay); } function _close() { - backdropService.close(); + focusLockService.removeInertAttribute(); + var tourIsOpen = document.body.classList.contains('umb-tour-is-visible'); + if (!tourIsOpen) { + backdropService.close(); + } currentOverlay = null; eventsService.emit('appState.overlay', null); } @@ -7124,7 +9054,13 @@ When building a custom infinite editor view you can use the same components as a if (!overlay.submitButtonStyle) overlay.submitButtonStyle = 'danger'; if (!overlay.submitButtonLabelKey) - overlay.submitButtonLabelKey = 'contentTypeEditor_yesDelete'; + overlay.submitButtonLabelKey = 'actions_delete'; + break; + case 'remove': + if (!overlay.submitButtonStyle) + overlay.submitButtonStyle = 'primary'; + if (!overlay.submitButtonLabelKey) + overlay.submitButtonLabelKey = 'actions_remove'; break; default: if (!overlay.submitButtonLabelKey) @@ -7136,12 +9072,17 @@ When building a custom infinite editor view you can use the same components as a overlay.confirmType = 'delete'; confirm(overlay); } + function confirmRemove(overlay) { + overlay.confirmType = 'remove'; + confirm(overlay); + } var service = { open: open, close: _close, ysod: ysod, confirm: confirm, - confirmDelete: confirmDelete + confirmDelete: confirmDelete, + confirmRemove: confirmRemove }; return service; } @@ -7206,7 +9147,7 @@ When building a custom infinite editor view you can use the same components as a push: function push(retryItem) { retryQueue.push(retryItem); // Call all the onItemAdded callbacks - angular.forEach(service.onItemAddedCallbacks, function (cb) { + Utilities.forEach(service.onItemAddedCallbacks, function (cb) { try { cb(retryItem); } catch (e) { @@ -7288,7 +9229,7 @@ When building a custom infinite editor view you can use the same components as a * *
        *      searchService.searchMembers({term: 'bob'}).then(function(results){
      - *          angular.forEach(results, function(result){
      + *          results.forEach(function(result){
        *                  //returns:
        *                  {name: "name", id: 1234, menuUrl: "url", editorPath: "url", metaData: {}, subtitle: "/path/etc" }
        *           })
      @@ -7296,7 +9237,7 @@ When building a custom infinite editor view you can use the same components as a
        *       })
        * 
      */ - angular.module('umbraco.services').factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper, $injector, searchResultFormatter) { + angular.module('umbraco.services').factory('searchService', function (entityResource, $injector, searchResultFormatter) { return { /** * @ngdoc method @@ -7314,8 +9255,8 @@ When building a custom infinite editor view you can use the same components as a throw 'args.term is required'; } return entityResource.search(args.term, 'Member', args.searchFrom).then(function (data) { - _.each(data, function (item) { - searchResultFormatter.configureMemberResult(item); + data.forEach(function (item) { + return searchResultFormatter.configureMemberResult(item); }); return data; }); @@ -7336,8 +9277,8 @@ When building a custom infinite editor view you can use the same components as a throw 'args.term is required'; } return entityResource.search(args.term, 'Document', args.searchFrom, args.canceler, args.dataTypeKey).then(function (data) { - _.each(data, function (item) { - searchResultFormatter.configureContentResult(item); + data.forEach(function (item) { + return searchResultFormatter.configureContentResult(item); }); return data; }); @@ -7358,8 +9299,8 @@ When building a custom infinite editor view you can use the same components as a throw 'args.term is required'; } return entityResource.search(args.term, 'Media', args.searchFrom, args.canceler, args.dataTypeKey).then(function (data) { - _.each(data, function (item) { - searchResultFormatter.configureMediaResult(item); + data.forEach(function (item) { + return searchResultFormatter.configureMediaResult(item); }); return data; }); @@ -7376,11 +9317,12 @@ When building a custom infinite editor view you can use the same components as a * @returns {Promise} returns promise containing all matching items */ searchAll: function searchAll(args) { + var _this = this; if (!args.term) { throw 'args.term is required'; } return entityResource.searchAll(args.term, args.canceler).then(function (data) { - _.each(data, function (resultByType) { + Object.values(data).forEach(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 @@ -7399,8 +9341,8 @@ When building a custom infinite editor view you can use the same components as a } } //now apply the formatter for each result - _.each(resultByType.results, function (item) { - formatterMethod.apply(this, [ + resultByType.results.forEach(function (item) { + formatterMethod.apply(_this, [ item, resultByType.treeAlias, resultByType.appAlias @@ -7420,7 +9362,7 @@ When building a custom infinite editor view you can use the same components as a function searchResultFormatter(umbRequestHelper) { function configureDefaultResult(content, treeAlias, appAlias) { content.editorPath = appAlias + '/' + treeAlias + '/edit/' + content.id; - angular.extend(content.metaData, { treeAlias: treeAlias }); + Utilities.extend(content.metaData, { treeAlias: treeAlias }); } function configureContentResult(content, treeAlias, appAlias) { content.menuUrl = umbRequestHelper.getApiUrl('contentTreeBaseUrl', 'GetMenu', [ @@ -7428,7 +9370,7 @@ When building a custom infinite editor view you can use the same components as a { application: appAlias } ]); content.editorPath = appAlias + '/' + treeAlias + '/edit/' + content.id; - angular.extend(content.metaData, { treeAlias: treeAlias }); + Utilities.extend(content.metaData, { treeAlias: treeAlias }); content.subTitle = content.metaData.Url; } function configureMemberResult(member, treeAlias, appAlias) { @@ -7437,7 +9379,7 @@ When building a custom infinite editor view you can use the same components as a { application: appAlias } ]); member.editorPath = appAlias + '/' + treeAlias + '/edit/' + (member.key ? member.key : member.id); - angular.extend(member.metaData, { treeAlias: treeAlias }); + Utilities.extend(member.metaData, { treeAlias: treeAlias }); member.subTitle = member.metaData.Email; } function configureMediaResult(media, treeAlias, appAlias) { @@ -7446,7 +9388,7 @@ When building a custom infinite editor view you can use the same components as a { application: appAlias } ]); media.editorPath = appAlias + '/' + treeAlias + '/edit/' + media.id; - angular.extend(media.metaData, { treeAlias: treeAlias }); + Utilities.extend(media.metaData, { treeAlias: treeAlias }); } return { configureContentResult: configureContentResult, @@ -7484,12 +9426,50 @@ When building a custom infinite editor view you can use the same components as a }); return deferred.promise; } - var service = { getSectionsForUser: getSectionsForUser }; - return service; + var service = { getSectionsForUser: getSectionsForUser }; + return service; + } + angular.module('umbraco.services').factory('sectionService', sectionService); + }()); + 'use strict'; + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); + } + function _nonIterableRest() { + throw new TypeError('Invalid attempt to destructure non-iterable instance'); + } + function _iterableToArrayLimit(arr, i) { + if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === '[object Arguments]')) { + return; + } + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + if (i && _arr.length === i) + break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i['return'] != null) + _i['return'](); + } finally { + if (_d) + throw _e; + } } - angular.module('umbraco.services').factory('sectionService', sectionService); - }()); - 'use strict'; + return _arr; + } + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) + return arr; + } /** * @ngdoc service * @name umbraco.services.serverValidationManager @@ -7499,78 +9479,518 @@ When building a custom infinite editor view you can use the same components as a * 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. + * + * For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: + * https://github.com/umbraco/Umbraco-CMS/pull/8339 + * */ function serverValidationManager($timeout) { + // The array of callback objects, each object is: + // - propertyAlias (this is the property's 'path' if it's a nested error) + // - culture + // - fieldName + // - segment + // - callback (function) + // - id (unique identifier, auto-generated, used internally for unsubscribing the callback) + // - options (used for complex properties, can contain options.matchType which can be either "suffix" or "prefix" or "contains") var callbacks = []; + // The array of error message objects, each object 'key' is: + // - propertyAlias (this is the property's 'path' if it's a nested error) + // - culture + // - fieldName + // - segment + // The object also contains: + // - errorMsg + var errorMsgItems = []; + var defaultMatchOptions = { matchType: null }; /** calls the callback specified with the errors specified, used internally */ - function executeCallback(self, errorsForCallback, callback, culture) { - callback.apply(self, [ - false, + function executeCallback(errorsForCallback, callback, culture, segment, isValid) { + callback.apply(instance, [ + isValid, // pass in a value indicating it is invalid errorsForCallback, // pass in the errors for this item - self.items, + errorMsgItems, // pass in all errors in total - culture // pass the culture that we are listing for. + culture, + // pass the culture that we are listing for. + segment // pass the segment that we are listing for. ]); } - function getFieldErrors(self, fieldName) { - if (!angular.isString(fieldName)) { + /** + * @ngdoc function + * @name notify + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Notifies all subscriptions again. Called when there are changes to subscriptions or errors. + */ + function notify() { + $timeout(function () { + for (var i = 0; i < errorMsgItems.length; i++) { + var item = errorMsgItems[i]; + } + notifyCallbacks(); + }); + } + function getFieldErrors(fieldName) { + if (!Utilities.isString(fieldName)) { throw 'fieldName must be a string'; } //find errors for this field name - return _.filter(self.items, function (item) { + return _.filter(errorMsgItems, function (item) { return item.propertyAlias === null && item.culture === 'invariant' && item.fieldName === fieldName; }); } - function getPropertyErrors(self, propertyAlias, culture, fieldName) { - if (!angular.isString(propertyAlias)) { + function getPropertyErrors(propertyAlias, culture, segment, fieldName, options) { + if (!Utilities.isString(propertyAlias)) { throw 'propertyAlias must be a string'; } - if (fieldName && !angular.isString(fieldName)) { + if (fieldName && !Utilities.isString(fieldName)) { throw 'fieldName must be a string'; } if (!culture) { culture = 'invariant'; } + if (!segment) { + segment = null; + } + if (!options) { + options = defaultMatchOptions; + } //find all errors for this property - return _.filter(self.items, function (item) { - return item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || fieldName === undefined || fieldName === ''); + return _.filter(errorMsgItems, function (errMsgItem) { + if (!errMsgItem.propertyAlias) { + return false; + } + var matchProp = matchErrMsgItemProperty(errMsgItem, propertyAlias, options); + return matchProp && errMsgItem.culture === culture && errMsgItem.segment === segment // ignore field matching if match options are used +&& (options.matchType || errMsgItem.fieldName === fieldName || fieldName === undefined || fieldName === ''); }); } - function getCultureErrors(self, culture) { + /** + * Returns true if the error message item's data matches the property validation key with a match type provided by the options + * @param {any} errMsgItem The error message item + * @param {any} propertyValidationKey The property validation key) + * @param {any} options The match type options + */ + function matchErrMsgItemProperty(errMsgItem, propertyValidationKey, options) { + if (errMsgItem.propertyAlias === propertyValidationKey) { + return true; + } + if (options.matchType === 'prefix' && errMsgItem.propertyAlias.startsWith(propertyValidationKey + '/')) { + return true; + } + if (options.matchType === 'suffix' && errMsgItem.propertyAlias.endsWith('/' + propertyValidationKey)) { + return true; + } + if (options.matchType === 'contains' && errMsgItem.propertyAlias.includes('/' + propertyValidationKey + '/')) { + return true; + } + return false; + } + function getVariantErrors(culture, segment) { if (!culture) { culture = 'invariant'; } + if (!segment) { + segment = null; + } //find all errors for this property - return _.filter(self.items, function (item) { - return item.culture === culture; + return _.filter(errorMsgItems, function (item) { + return item.culture === culture && item.segment === segment; }); } - function notifyCallbacks(self) { - for (var cb in callbacks) { - if (callbacks[cb].propertyAlias === null && callbacks[cb].fieldName !== null) { - //its a field error callback - var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName); - if (fieldErrors.length > 0) { - executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture); - } - } else if (callbacks[cb].propertyAlias != null) { - //its a property error - var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].fieldName); - if (propErrors.length > 0) { - executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture); + function notifyCallback(cb) { + if (cb.propertyAlias === null && cb.fieldName !== null) { + //its a field error callback + var fieldErrors = getFieldErrors(cb.fieldName); + var valid = fieldErrors.length === 0; + executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment, valid); + } else if (cb.propertyAlias != null) { + //its a property error + var propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName, cb.options); + var _valid = propErrors.length === 0; + executeCallback(propErrors, cb.callback, cb.culture, cb.segment, _valid); + } else { + //its a variant error + var variantErrors = getVariantErrors(cb.culture, cb.segment); + var _valid2 = variantErrors.length === 0; + executeCallback(variantErrors, cb.callback, cb.culture, cb.segment, _valid2); + } + } + /** Call all registered callbacks indicating if the data they are subscribed to is valid or invalid */ + function notifyCallbacks() { + // nothing to call + if (errorMsgItems.length === 0) { + return; + } + callbacks.forEach(function (cb) { + return notifyCallback(cb); + }); + } + /** + * Flattens the complex errror result json into an array of the block's id/parent id and it's corresponding validation ModelState + * @param {any} errorMsg + * @param {any} parentPropertyAlias The parent property alias for the json error + */ + function parseComplexEditorError(errorMsg, parentPropertyAlias) { + var json = Utilities.isArray(errorMsg) ? errorMsg : JSON.parse(errorMsg); + var result = []; + function extractModelState(validation, parentPath) { + if (validation.$id && validation.ModelState) { + var ms = { + validationPath: ''.concat(parentPath, '/').concat(validation.$id), + modelState: validation.ModelState + }; + result.push(ms); + return ms; + } + return null; + } + function iterateErrorBlocks(blocks, parentPath) { + for (var i = 0; i < blocks.length; i++) { + var validation = blocks[i]; + var ms = extractModelState(validation, parentPath); + if (!ms) { + continue; } - } else { - //its a culture error - var cultureErrors = getCultureErrors(self, callbacks[cb].culture); - if (cultureErrors.length > 0) { - executeCallback(self, cultureErrors, callbacks[cb].callback, callbacks[cb].culture); + var nested = _.omit(validation, '$id', '$elementTypeAlias', 'ModelState'); + for (var _i = 0, _Object$entries = Object.entries(nested); _i < _Object$entries.length; _i++) { + var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2), key = _Object$entries$_i[0], value = _Object$entries$_i[1]; + if (Array.isArray(value)) { + iterateErrorBlocks(value, ''.concat(ms.validationPath, '/').concat(key)); // recurse + } } } } + iterateErrorBlocks(json, parentPropertyAlias); + return result; } - return { + /** + * @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. + */ + function getPropertyCallbacks(propertyAlias, culture, fieldName, segment) { + //normalize culture to "invariant" + if (!culture) { + culture = 'invariant'; + } + //normalize segment to null + if (!segment) { + segment = null; + } + var found = _.filter(callbacks, function (cb) { + if (!cb.options) { + cb.options = defaultMatchOptions; + } + var matchProp = matchCallbackItemProperty(cb, propertyAlias); + //returns any callback that have been registered directly against the field and for only the property + return matchProp && cb.culture === culture && cb.segment === segment // ignore field matching if match options are used +&& (cb.options.matchType || cb.fieldName === fieldName || cb.fieldName === undefined || cb.fieldName === ''); + }); + return found; + } + /** + * Returns true if the callback item's data and match options matches the property validation key + * @param {any} cb + * @param {any} propertyValidationKey + */ + function matchCallbackItemProperty(cb, propertyValidationKey) { + if (cb.propertyAlias === propertyValidationKey) { + return true; + } + if (cb.options.matchType === 'prefix' && propertyValidationKey.startsWith(cb.propertyAlias + '/')) { + return true; + } + if (cb.options.matchType === 'suffix' && propertyValidationKey.endsWith('/' + cb.propertyAlias)) { + return true; + } + if (cb.options.matchType === 'contains' && propertyValidationKey.includes('/' + cb.propertyAlias + '/')) { + return true; + } + return false; + } + /** + * @ngdoc function + * @name getFieldCallbacks + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets all callbacks that has been registered using the subscribe method for the field. + */ + function getFieldCallbacks(fieldName) { + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly against the field + return item.propertyAlias === null && item.culture === 'invariant' && item.segment === null && item.fieldName === fieldName; + }); + return found; + } + /** + * @ngdoc function + * @name getVariantCallbacks + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets all callbacks that has been registered using the subscribe method for the culture and segment. + */ + function getVariantCallbacks(culture, segment) { + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly against the given culture and given segment. + return item.culture === culture && item.segment === segment && item.propertyAlias === null && item.fieldName === null; + }); + 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') + */ + function addFieldError(fieldName, errorMsg) { + if (!fieldName) { + return; + } + //only add the item if it doesn't exist + if (!hasFieldError(fieldName)) { + errorMsgItems.push({ + propertyAlias: null, + culture: 'invariant', + segment: null, + fieldName: fieldName, + errorMsg: errorMsg + }); + } + //find all errors for this item + var errorsForCallback = getFieldErrors(fieldName); + //we should now call all of the call backs registered for this error + var cbs = getFieldCallbacks(fieldName); + //call each callback for this error + for (var cb in cbs) { + executeCallback(errorsForCallback, cbs[cb].callback, null, null, false); + } + } + /** + * @ngdoc function + * @name addPropertyError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Adds an error message for the content property + */ + function _addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment) { + if (!propertyAlias) { + return; + } + //normalize culture to "invariant" + if (!culture) { + culture = 'invariant'; + } + //normalize segment to null + if (!segment) { + segment = null; + } + //normalize errorMsg to empty + if (!errorMsg) { + errorMsg = ''; + } + // remove all non printable chars and whitespace from the string + // (this can be a json string for complex errors and in some test cases contains odd whitespace) + if (Utilities.isString(errorMsg)) { + errorMsg = errorMsg.trimStartSpecial().trim(); + } + // if the error message is json it's a complex editor validation response that we need to parse + if (Utilities.isString(errorMsg) && errorMsg.startsWith('[') || Utilities.isArray(errorMsg)) { + // flatten the json structure, create validation paths for each property and add each as a property error + var idsToErrors = parseComplexEditorError(errorMsg, propertyAlias); + idsToErrors.forEach(function (x) { + return addErrorsForModelState(x.modelState, x.validationPath); + }); + // We need to clear the error message else it will show up as a giant json block against the property + errorMsg = ''; + } + //only add the item if it doesn't exist + if (!hasPropertyError(propertyAlias, culture, fieldName, segment)) { + errorMsgItems.push({ + propertyAlias: propertyAlias, + culture: culture, + segment: segment, + fieldName: fieldName, + errorMsg: errorMsg + }); + } + } + /** + * @ngdoc function + * @name hasPropertyError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Checks if the content property + culture + field name combo has an error + */ + function hasPropertyError(propertyAlias, culture, fieldName, segment) { + //normalize culture to null + if (!culture) { + culture = 'invariant'; + } + //normalize segment to null + if (!segment) { + segment = null; + } + var err = _.find(errorMsgItems, 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.culture === culture && item.segment === segment && (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 + */ + function hasFieldError(fieldName) { + var err = _.find(errorMsgItems, 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.culture === 'invariant' && item.segment === null && item.fieldName === fieldName; + }); + return err ? true : false; + } + /** + * @ngdoc function + * @name addErrorsForModelState + * @methodOf umbraco.services.serverValidationManager + * @param {any} modelState the modelState object + * @param {any} parentValidationPath optional parameter specifying a nested element's UDI for which this property belongs (for complex editors) + * @description + * This wires up all of the server validation model state so that valServer and valServerField directives work + */ + function addErrorsForModelState(modelState, parentValidationPath) { + if (!Utilities.isObject(modelState)) { + throw 'modelState is not an object'; + } + for (var _i2 = 0, _Object$entries2 = Object.entries(modelState); _i2 < _Object$entries2.length; _i2++) { + var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i2], 2), key = _Object$entries2$_i[0], value = _Object$entries2$_i[1]; + //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 there's some special ModelState syntax we need to know about. + // 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 for a content 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 4 parts for content 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. + // Example: "_Properties.headerImage.en-US.mySegment.myField" + // * it's for a property since it has a _Properties prefix + // * it's for the headerImage property type + // * it's for the en-US culture + // * it's for the mySegment segment + // * it's for the myField html field (optional) + var parts = key.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') { + // create the validation key, might just be the prop alias but if it's nested will be validation path + // like "myBlockEditor/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/7170A4DD-2441-4B1B-A8D3-437D75C4CBC9/city" + var propertyValidationKey = createPropertyValidationKey(parts[1], parentValidationPath); + var culture = null; + if (parts.length > 2) { + culture = parts[2]; + //special check in case the string is formatted this way + if (culture === 'null') { + culture = null; + } + } + var segment = null; + if (parts.length > 3) { + segment = parts[3]; + //special check in case the string is formatted this way + if (segment === 'null') { + segment = null; + } + } + var htmlFieldReference = ''; + if (parts.length > 4) { + htmlFieldReference = parts[4] || ''; + } + // add a generic error for the property + _addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment); + } else { + //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: + // Groups[0].Properties[2].Alias + addFieldError(key, value[0]); + } + } + if (hasPropertyError) { + // ensure all callbacks are called after property errors are added + notifyCallbacks(); + } + } + function createPropertyValidationKey(propertyAlias, parentValidationPath) { + return parentValidationPath ? parentValidationPath + '/' + propertyAlias : propertyAlias; + } + /** + * @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 + */ + function reset() { + clear(); + for (var cb in callbacks) { + callbacks[cb].callback.apply(instance, [ + true, + //pass in a value indicating it is VALID + [], + //pass in empty collection + [], + null, + null + ]); + } + } + /** + * @ngdoc function + * @name clear + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Clears all errors + */ + function clear() { + errorMsgItems = []; + } + var instance = { + addErrorsForModelState: addErrorsForModelState, + parseComplexEditorError: parseComplexEditorError, + createPropertyValidationKey: createPropertyValidationKey, /** * @ngdoc function * @name notifyAndClearAllSubscriptions @@ -7578,38 +9998,25 @@ When building a custom infinite editor view you can use the same components as a * @function * * @description - * This method needs to be called once all field and property errors are wired up. + * This method can 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. + * + * In the case of content with complex editors, variants and different views, those editors don't call this method and instead + * manage the server validation manually by calling notify when necessary and clear/reset when necessary. */ notifyAndClearAllSubscriptions: function notifyAndClearAllSubscriptions() { - var self = this; $timeout(function () { - notifyCallbacks(self); + notifyCallbacks(); //now that they are all executed, we're gonna clear all of the errors we have - self.clear(); - }); - }, - /** - * @ngdoc function - * @name notify - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * This method isn't used very often but can be used if all subscriptions need to be notified again. This can be - * handy if a view needs to be reloaded/rebuild like when switching variants in the content editor. - */ - notify: function notify() { - var self = this; - $timeout(function () { - notifyCallbacks(self); + clear(); }); }, + notify: notify, /** * @ngdoc function * @name subscribe @@ -7623,246 +10030,125 @@ When building a custom infinite editor view you can use the same components as a * field alias to listen for. * If propertyAlias is null, then this subscription is for a field property (not a user defined property). */ - subscribe: function subscribe(propertyAlias, culture, fieldName, callback) { + subscribe: function subscribe(propertyAlias, culture, fieldName, callback, segment, options) { if (!callback) { return; } var id = String.CreateGuid(); + //normalize culture to "invariant" if (!culture) { culture = 'invariant'; } + //normalize segment to null + if (!segment) { + segment = null; + } + var cb = null; if (propertyAlias === null) { - callbacks.push({ + cb = { propertyAlias: null, culture: culture, + segment: segment, fieldName: fieldName, callback: callback, id: id - }); - } else if (propertyAlias !== undefined) { - //normalize culture to null - callbacks.push({ - propertyAlias: propertyAlias, - culture: culture, - fieldName: fieldName, - callback: callback, - id: id - }); - } - function unsubscribeId() { - //remove all callbacks for the content field - callbacks = _.reject(callbacks, function (item) { - return item.id === id; - }); - } - //return a function to unsubscribe this subscription by uniqueId - return unsubscribeId; - }, - /** - * Removes all callbacks registered for the propertyALias, culture and fieldName combination - * @param {} propertyAlias - * @param {} culture - * @param {} fieldName - * @returns {} - */ - unsubscribe: function unsubscribe(propertyAlias, culture, fieldName) { - //normalize culture to null - if (!culture) { - culture = 'invariant'; - } - if (propertyAlias === null) { - //remove all callbacks for the content field - callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === null && item.culture === culture && item.fieldName === fieldName; - }); + }; } else if (propertyAlias !== undefined) { - //remove all callbacks for the content property - callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === propertyAlias && item.culture === culture && (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 getPropertyCallbacks(propertyAlias, culture, fieldName) { - //normalize culture to null - if (!culture) { - culture = 'invariant'; - } - 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.culture === culture && (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 getFieldCallbacks(fieldName) { - var found = _.filter(callbacks, function (item) { - //returns any callback that have been registered directly against the field - return item.propertyAlias === null && item.culture === 'invariant' && item.fieldName === fieldName; - }); - return found; - }, - /** - * @ngdoc function - * @name getCultureCallbacks - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Gets all callbacks that has been registered using the subscribe method for the culture. - */ - getCultureCallbacks: function getCultureCallbacks(culture) { - var found = _.filter(callbacks, function (item) { - //returns any callback that have been registered directly/ONLY against the culture - return item.culture === culture && item.propertyAlias === null && item.fieldName === null; - }); - 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 addFieldError(fieldName, errorMsg) { - if (!fieldName) { - return; - } - //only add the item if it doesn't exist - if (!this.hasFieldError(fieldName)) { - this.items.push({ - propertyAlias: null, - culture: 'invariant', - 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, null); - } - }, - /** - * @ngdoc function - * @name addPropertyError - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Adds an error message for the content property - */ - addPropertyError: function addPropertyError(propertyAlias, culture, fieldName, errorMsg) { - if (!propertyAlias) { - return; - } - //normalize culture to "invariant" - if (!culture) { - culture = 'invariant'; - } - //only add the item if it doesn't exist - if (!this.hasPropertyError(propertyAlias, culture, fieldName)) { - this.items.push({ + cb = { propertyAlias: propertyAlias, culture: culture, + segment: segment, fieldName: fieldName, - errorMsg: errorMsg - }); - } - //find all errors for this item - var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, fieldName); - //we should now call all of the call backs registered for this error - var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName); - //call each callback for this error - for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback, culture); + callback: callback, + id: id, + options: options + }; } - //execute culture specific callbacks here too when a propery error is added - var cultureCbs = this.getCultureCallbacks(culture); - //call each callback for this error - for (var cb in cultureCbs) { - executeCallback(this, errorsForCallback, cultureCbs[cb].callback, culture); + callbacks.push(cb); + function unsubscribeId() { + //remove all callbacks for the content field + callbacks = _.reject(callbacks, function (item) { + return item.id === id; + }); } + // Notify the new callback + notifyCallback(cb); + //return a function to unsubscribe this subscription by uniqueId + return unsubscribeId; }, /** - * @ngdoc function - * @name removePropertyError - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Removes an error message for the content property + * Removes all callbacks registered for the propertyALias, culture and fieldName combination + * @param {} propertyAlias + * @param {} culture + * @param {} fieldName + * @returns {} */ - removePropertyError: function removePropertyError(propertyAlias, culture, fieldName) { - if (!propertyAlias) { - return; - } - //normalize culture to null + unsubscribe: function unsubscribe(propertyAlias, culture, fieldName, segment) { + //normalize culture to "invariant" if (!culture) { culture = 'invariant'; } - //remove the item - this.items = _.reject(this.items, function (item) { - return item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || fieldName === undefined || fieldName === ''); - }); + //normalize segment to null + if (!segment) { + segment = null; + } + if (propertyAlias === null) { + //remove all callbacks for the content field + callbacks = _.reject(callbacks, function (item) { + return item.propertyAlias === null && item.culture === culture && item.segment === segment && item.fieldName === fieldName; + }); + } else if (propertyAlias !== undefined) { + //remove all callbacks for the content property + callbacks = _.reject(callbacks, function (item) { + return item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === '') && (fieldName === undefined || fieldName === '')); + }); + } }, + getPropertyCallbacks: getPropertyCallbacks, + getFieldCallbacks: getFieldCallbacks, /** * @ngdoc function - * @name reset + * @name getCultureCallbacks * @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 + * Gets all callbacks that has been registered using the subscribe method for the culture. Not including segments. */ - reset: function reset() { - 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 - } + getCultureCallbacks: function getCultureCallbacks(culture) { + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly/ONLY against the culture + return item.culture === culture && item.segment === null && item.propertyAlias === null && item.fieldName === null; + }); + return found; + }, + getVariantCallbacks: getVariantCallbacks, + addFieldError: addFieldError, + addPropertyError: function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment) { + _addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment); + notifyCallbacks(); // ensure all callbacks are called }, /** * @ngdoc function - * @name clear + * @name removePropertyError * @methodOf umbraco.services.serverValidationManager * @function * * @description - * Clears all errors + * Removes an error message for the content property */ - clear: function clear() { - this.items = []; + removePropertyError: function removePropertyError(propertyAlias, culture, fieldName, segment, options) { + var errors = getPropertyErrors(propertyAlias, culture, segment, fieldName, options); + errorMsgItems = errorMsgItems.filter(function (v) { + return errors.indexOf(v) === -1; + }); + if (errors.length > 0) { + // removal was successful, re-notify all subscribers + notifyCallbacks(); + } }, + reset: reset, + clear: clear, /** * @ngdoc function * @name getPropertyError @@ -7872,16 +10158,16 @@ When building a custom infinite editor view you can use the same components as a * @description * Gets the error message for the content property */ - getPropertyError: function getPropertyError(propertyAlias, culture, fieldName) { - //normalize culture to null - if (!culture) { - culture = 'invariant'; + getPropertyError: function getPropertyError(propertyAlias, culture, fieldName, segment) { + var errors = getPropertyErrors(propertyAlias, culture, segment, fieldName); + if (errors.length > 0) { + // should only ever contain one + return errors[0]; } - 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.culture === culture && (item.fieldName === fieldName || fieldName === undefined || fieldName === ''); - }); - return err; + return undefined; + }, + getPropertyErrorsByValidationPath: function getPropertyErrorsByValidationPath(propertyValidationPath, culture, segment, options) { + return getPropertyErrors(propertyValidationPath, culture, segment, '', options); }, /** * @ngdoc function @@ -7893,70 +10179,67 @@ When building a custom infinite editor view you can use the same components as a * Gets the error message for a content field */ getFieldError: function getFieldError(fieldName) { - var err = _.find(this.items, function (item) { + var err = _.find(errorMsgItems, 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.culture === 'invariant' && item.fieldName === fieldName; + return item.propertyAlias === null && item.culture === 'invariant' && item.segment === null && item.fieldName === fieldName; }); return err; }, + hasPropertyError: hasPropertyError, + hasFieldError: hasFieldError, /** * @ngdoc function - * @name hasPropertyError + * @name hasCultureError * @methodOf umbraco.services.serverValidationManager * @function * * @description - * Checks if the content property + culture + field name combo has an error + * Checks if the given culture has an error */ - hasPropertyError: function hasPropertyError(propertyAlias, culture, fieldName) { - //normalize culture to null + hasCultureError: function hasCultureError(culture) { + //normalize culture to "invariant" if (!culture) { culture = 'invariant'; } - 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.culture === culture && (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 hasFieldError(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.culture === 'invariant' && item.fieldName === fieldName; + var err = _.find(errorMsgItems, function (item) { + return item.culture === culture && item.segment === null; }); return err ? true : false; }, /** * @ngdoc function - * @name hasCultureError + * @name hasVariantError * @methodOf umbraco.services.serverValidationManager * @function * * @description * Checks if the given culture has an error */ - hasCultureError: function hasCultureError(culture) { - //normalize culture to null + hasVariantError: function hasVariantError(culture, segment) { + //normalize culture to "invariant" if (!culture) { culture = 'invariant'; } - var err = _.find(this.items, function (item) { - return item.culture === culture; + //normalize segment to null + if (!segment) { + segment = null; + } + var err = _.find(errorMsgItems, function (item) { + return item.culture === culture && item.segment === segment; }); return err ? true : false; - }, - /** The array of error messages */ - items: [] + } }; + // Used to return the 'items' array as a reference/getter + Object.defineProperty(instance, 'items', { + get: function get() { + return errorMsgItems; + }, + set: function set(value) { + throw 'Cannot set the items array'; + } + }); + return instance; } angular.module('umbraco.services').factory('serverValidationManager', serverValidationManager); 'use strict'; @@ -8476,7 +10759,7 @@ When building a custom infinite editor view you can use the same components as a function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService) { //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|lang]'; var fallbackStyles = [ { title: 'Page header', @@ -8505,25 +10788,85 @@ When building a custom infinite editor view you can use the same components as a ]; // these languages are available for localization var availableLanguages = [ + 'ar', + 'ar_SA', + 'hy', + 'az', + 'eu', + 'be', + 'bn_BD', + 'bs', + 'bg_BG', + 'ca', + 'zh_CN', + 'zh_TW', + 'hr', + 'cs', 'da', - 'de', - 'en', - 'en_us', + 'dv', + 'nl', + 'en_CA', + 'en_GB', + 'et', + 'fo', 'fi', - 'fr', - 'he', + 'fr_FR', + 'gd', + 'gl', + 'ka_GE', + 'de', + 'de_AT', + 'el', + 'he_IL', + 'hi_IN', + 'hu_HU', + 'is_IS', + 'id', 'it', 'ja', - 'nl', - 'no', + 'kab', + 'kk', + 'km_KH', + 'ko_KR', + 'ku', + 'ku_IQ', + 'lv', + 'lt', + 'lb', + 'ml', + 'ml_IN', + 'mn_MN', + 'nb_NO', + 'fa', + 'fa_IR', 'pl', - 'pt', + 'pt_BR', + 'pt_PT', + 'ro', 'ru', - 'sv', - 'zh' + 'sr', + 'si_LK', + 'sk', + 'sl_SI', + 'es', + 'es_MX', + 'sv_SE', + 'tg', + 'ta', + 'ta_IN', + 'tt', + 'th_TH', + 'tr', + 'tr_TR', + 'ug', + 'uk', + 'uk_UA', + 'vi', + 'vi_VN', + 'cy' ]; //define fallback language - var defaultLanguage = 'en_us'; + var defaultLanguage = 'en_US'; /** * Returns a promise of an object containing the stylesheets and styleFormats collections * @param {any} configuredStylesheets @@ -8535,7 +10878,7 @@ When building a custom infinite editor view you can use the same components as a //a collection of promises, the first one is an empty promise //queue rules loading if (configuredStylesheets) { - angular.forEach(configuredStylesheets, function (val, key) { + configuredStylesheets.forEach(function (val, key) { if (val.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + '/') === 0) { // current format (full path to stylesheet) stylesheets.push(val); @@ -8544,7 +10887,7 @@ When building a custom infinite editor view you can use the same components as a stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + '/' + val + '.css'); } promises.push(stylesheetResource.getRulesByName(val).then(function (rules) { - angular.forEach(rules, function (rule) { + rules.forEach(function (rule) { var r = {}; r.title = rule.name; if (rule.selector[0] == '.') { @@ -8556,7 +10899,7 @@ When building a custom infinite editor view you can use the same components as a } 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('.', ' '); + r.classes = rule.selector.substring(rule.selector.indexOf('.') + 1).replace(/\./g, ' '); } else if (rule.selector[0] != '#' && rule.selector.indexOf('#') > -1) { var split = rule.selector.split('#'); r.block = split[0]; @@ -8589,7 +10932,7 @@ When building a custom infinite editor view you can use the same components as a var localeId = $locale.id.replace('-', '_'); //try matching the language using full locale format var languageMatch = _.find(availableLanguages, function (o) { - return o === localeId; + return o.toLowerCase() === localeId; }); //if no matches, try matching using only the language if (languageMatch === undefined) { @@ -8695,6 +11038,7 @@ When building a custom infinite editor view you can use the same components as a var src = imgUrl + '?width=' + newSize.width + '&height=' + newSize.height; editor.dom.setAttrib(imageDomElement, 'data-mce-src', src); } + editor.execCommand('mceAutoResize', false, null, null); } } function isMediaPickerEnabled(toolbarItemArray) { @@ -8728,15 +11072,19 @@ When building a custom infinite editor view you can use the same components as a var plugins = _.map(tinyMceConfig.plugins, function (plugin) { return plugin.name; }); - //plugins that must always be active + // Plugins that must always be active plugins.push('autoresize'); plugins.push('noneditable'); + // Table plugin use color picker plugin in table properties + if (plugins.includes('table')) { + plugins.push('colorpicker'); + } var modeTheme = ''; var modeInline = false; - //Based on mode set - //classic = Theme: modern, inline: false - //inline = Theme: modern, inline: true, - //distraction-free = Theme: inlite, inline: true + // Based on mode set + // classic = Theme: modern, inline: false + // inline = Theme: modern, inline: true, + // distraction-free = Theme: inlite, inline: true switch (args.mode) { case 'classic': modeTheme = 'modern'; @@ -8795,8 +11143,7 @@ When building a custom infinite editor view you can use the same components as a // We are not ready to limit the pasted elements further than default, we will return to this feature. ( TODO: Make this feature an option. ) // We keep spans here, cause removing spans here also removes b-tags inside of them, instead we strip them out later. (TODO: move this definition to the config file... ) var validPasteElements = "-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody,img[src|alt|width|height],ul,ol,li,hr,pre,dl,dt,figure,figcaption,wbr" - - // add elements from user configurated styleFormats to our list of validPasteElements. + // add elements from user configurated styleFormats to our list of validPasteElements. // (This means that we only allow H3-element if its configured as a styleFormat on this specific propertyEditor.) var style, i = 0; for(; i < styles.styleFormats.length; i++) { @@ -8817,7 +11164,7 @@ When building a custom infinite editor view you can use the same components as a //paste_word_valid_elements: validPasteElements, paste_preprocess: cleanupPasteData }; - angular.extend(config, pasteConfig); + Utilities.extend(config, pasteConfig); 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 @@ -8831,8 +11178,8 @@ When building a custom infinite editor view you can use the same components as a //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(config[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { - //concat it and below this concat'd array will overwrite the baseline in angular.extend + if (Utilities.isArray(config[i]) && Utilities.isArray(tinyMceConfig.customConfig[i])) { + //concat it and below this concat'd array will overwrite the baseline in Utilities.extend tinyMceConfig.customConfig[i] = config[i].concat(tinyMceConfig.customConfig[i]); } } catch (e) { @@ -8846,7 +11193,7 @@ When building a custom infinite editor view you can use the same components as a } } } - angular.extend(config, tinyMceConfig.customConfig); + Utilities.extend(config, tinyMceConfig.customConfig); } return config; }); @@ -9553,6 +11900,8 @@ When building a custom infinite editor view you can use the same components as a //if (!args.model.value) { // throw "args.model.value is required"; //} + // force TinyMCE to load plugins/themes from minified files (see http://archive.tinymce.com/wiki.php/api4:property.tinymce.suffix.static) + args.editor.suffix = '.min'; var unwatch = null; //Starts a watch on the model value so that we can update TinyMCE if the model changes behind the scenes or from the server function startWatch() { @@ -9576,10 +11925,22 @@ When building a custom infinite editor view you can use the same components as a } } function syncContent() { + if (args.model.value === args.editor.getContent()) { + return; + } //stop watching before we update the value stopWatch(); angularHelper.safeApply($rootScope, function () { args.model.value = args.editor.getContent(); + //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. + if (args.currentForm) { + args.currentForm.$setDirty(); + } + // With complex validation we need to set a input field to dirty, not the form. but we will keep the old code for backwards compatibility. + if (args.currentFormInput) { + args.currentFormInput.$setDirty(); + } }); //re-watch the value startWatch(); @@ -9602,7 +11963,7 @@ When building a custom infinite editor view you can use the same components as a var content = e.content; // Upload BLOB images (dragged/pasted ones) // find src attribute where value starts with `blob:` - // search is case-insensitive and allows single or double quotes + // search is case-insensitive and allows single or double quotes if (content.search(/src=["']blob:.*?["']/gi) !== -1) { args.editor.uploadImages(function (data) { // Once all images have been uploaded @@ -9640,6 +12001,17 @@ When building a custom infinite editor view you can use the same components as a } }); } + if (Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true) { + /** prevent injecting arbitrary JavaScript execution in on-attributes. */ + var allNodes = Array.prototype.slice.call(args.editor.dom.doc.getElementsByTagName('*')); + allNodes.forEach(function (node) { + for (var i = 0; i < node.attributes.length; i++) { + if (node.attributes[i].name.indexOf('on') === 0) { + node.removeAttribute(node.attributes[i].name); + } + } + }); + } }); args.editor.on('init', function (e) { if (args.model.value) { @@ -9647,12 +12019,74 @@ When building a custom infinite editor view you can use the same components as a } //enable browser based spell checking args.editor.getBody().setAttribute('spellcheck', true); + /** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes: + * https://github.com/advisories/GHSA-w7jx-j77m-wp65 + * https://github.com/advisories/GHSA-5vm8-hhgr-jcjp + */ + var uriAttributesToSanitize = [ + 'src', + 'href', + 'data', + 'background', + 'action', + 'formaction', + 'poster', + 'xlink:href' + ]; + var parseUri = function () { + // Encapsulated JS logic. + var safeSvgDataUrlElements = [ + 'img', + 'video' + ]; + var scriptUriRegExp = /((java|vb)script|mhtml):/i; + var trimRegExp = /[\s\u0000-\u001F]+/g; + var isInvalidUri = function isInvalidUri(uri, tagName) { + if (/^data:image\//i.test(uri)) { + return safeSvgDataUrlElements.indexOf(tagName) !== -1 && /^data:image\/svg\+xml/i.test(uri); + } else { + return /^data:/i.test(uri); + } + }; + return function parseUri(uri, tagName) { + uri = uri.replace(trimRegExp, ''); + try { + // Might throw malformed URI sequence + uri = decodeURIComponent(uri); + } catch (ex) { + // Fallback to non UTF-8 decoder + uri = unescape(uri); + } + if (scriptUriRegExp.test(uri)) { + return; + } + if (isInvalidUri(uri, tagName)) { + return; + } + return uri; + }; + }(); + if (Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true) { + args.editor.serializer.addAttributeFilter(uriAttributesToSanitize, function (nodes) { + nodes.forEach(function (node) { + node.attributes.forEach(function (attr) { + var attrName = attr.name.toLowerCase(); + if (uriAttributesToSanitize.indexOf(attrName) !== -1) { + attr.value = parseUri(attr.value, node.name); + } + }); + }); + }); + } //start watching the value startWatch(); }); args.editor.on('Change', function (e) { syncContent(); }); + args.editor.on('Keyup', function (e) { + syncContent(); + }); //when we leave the editor (maybe) args.editor.on('blur', function (e) { syncContent(); @@ -9665,11 +12099,7 @@ When building a custom infinite editor view you can use the same components as a syncContent(); }); args.editor.on('Dirty', function (e) { - //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. - if (args.currentForm) { - args.currentForm.$setDirty(); - } + syncContent(); // Set model.value to the RTE's content }); var self = this; //create link picker @@ -9680,6 +12110,7 @@ When building a custom infinite editor view you can use the same components as a dataTypeKey: args.model.dataTypeKey, ignoreUserStartNodes: args.model.config.ignoreUserStartNodes, anchors: anchorValues, + size: args.model.config.overlaySize, submit: function submit(model) { self.insertLinkInEditor(args.editor, model.target, anchorElement); editorService.close(); @@ -9796,8 +12227,8 @@ When building a custom infinite editor view you can use the same components as a function registerAllTours() { tours = []; return tourResource.getTours().then(function (tourFiles) { - angular.forEach(tourFiles, function (tourFile) { - angular.forEach(tourFile.tours, function (newTour) { + Utilities.forEach(tourFiles, function (tourFile) { + Utilities.forEach(tourFile.tours, function (newTour) { validateTour(newTour); validateTourRegistration(newTour); tours.push(newTour); @@ -10006,7 +12437,7 @@ When building a custom infinite editor view you can use the same components as a */ function validateTourRegistration(tour) { // check for existing tours with the same alias - angular.forEach(tours, function (existingTour) { + Utilities.forEach(tours, function (existingTour) { if (existingTour.alias === tour.alias) { throw 'A tour with the alias ' + tour.alias + ' is already registered'; } @@ -10019,16 +12450,16 @@ When building a custom infinite editor view you can use the same components as a function setTourStatuses(tours) { var deferred = $q.defer(); currentUserResource.getTours().then(function (storedTours) { - angular.forEach(storedTours, function (storedTour) { + Utilities.forEach(storedTours, function (storedTour) { if (storedTour.completed === true) { - angular.forEach(tours, function (tour) { + Utilities.forEach(tours, function (tour) { if (storedTour.alias === tour.alias) { tour.completed = true; } }); } if (storedTour.disabled === true) { - angular.forEach(tours, function (tour) { + Utilities.forEach(tours, function (tour) { if (storedTour.alias === tour.alias) { tour.disabled = true; } @@ -10093,13 +12524,13 @@ When building a custom infinite editor view you can use the same components as a }, /** Internal method to track expanded paths on a tree */ _trackExpandedPaths: function _trackExpandedPaths(node, expandedPaths) { - if (!node.children || !angular.isArray(node.children) || node.children.length == 0) { + if (!node.children || !Utilities.isArray(node.children) || node.children.length == 0) { return; } //take the last child var childPath = this.getPath(node.children[node.children.length - 1]).join(','); //check if this already exists, if so exit - if (expandedPaths.indexOf(childPath) !== -1) { + if (expandedPaths.includes(childPath)) { return; } if (expandedPaths.length === 0) { @@ -10109,16 +12540,16 @@ When building a custom infinite editor view you can use the same components as a } var clonedPaths = expandedPaths.slice(0); //make a copy to iterate over so we can modify the original in the iteration - _.each(clonedPaths, function (p) { + clonedPaths.forEach(function (p) { if (childPath.startsWith(p + ',')) { //this means that the node's path supercedes this path stored so we can remove the current 'p' and replace it with node.path expandedPaths.splice(expandedPaths.indexOf(p), 1); //remove it - if (expandedPaths.indexOf(childPath) === -1) { + if (expandedPaths.includes(childPath) === false) { expandedPaths.push(childPath); //replace it } } else if (p.startsWith(childPath + ',')) { - } else if (expandedPaths.indexOf(childPath) === -1) { + } else if (expandedPaths.includes(childPath) === false) { expandedPaths.push(childPath); //track it } }); @@ -10165,7 +12596,7 @@ When building a custom infinite editor view you can use the same components as a if (treeNode.iconIsClass === undefined || treeNode.iconIsClass) { var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNode); treeNode.cssClass = standardCssClass + ' ' + converted; - if (converted.startsWith('.')) { + if (converted && converted.startsWith('.')) { //its legacy so add some width/height treeNode.style = 'height:16px;width:16px;'; } else { @@ -10198,7 +12629,7 @@ When building a custom infinite editor view you can use the same components as a */ getTreePackageFolder: function getTreePackageFolder(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)) { + if (Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.trees && Utilities.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) { return invariantEquals(item.alias, treeAlias); }); @@ -10253,7 +12684,7 @@ When building a custom infinite editor view you can use the same components as a return cc; } }); - } else if (args.filter && angular.isFunction(args.filter)) { + } else if (args.filter && Utilities.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'; @@ -10320,7 +12751,7 @@ When building a custom infinite editor view you can use the same components as a args.node.expanded = true; args.node.hasChildren = true; //Since we've removed the children & reloaded them, we need to refresh the UI now because the tree node UI doesn't operate on normal angular $watch since that will be pretty slow - if (angular.isFunction(args.node.updateNodeData)) { + if (Utilities.isFunction(args.node.updateNodeData)) { args.node.updateNodeData(args.node); } } @@ -10346,7 +12777,7 @@ When building a custom infinite editor view you can use the same components as a * @param {object} treeNode the node to remove */ removeNode: function removeNode(treeNode) { - if (!angular.isFunction(treeNode.parent)) { + if (!Utilities.isFunction(treeNode.parent)) { return; } if (treeNode.parent() == null) { @@ -10454,7 +12885,7 @@ When building a custom infinite editor view you can use the same components as a } 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) { + if (child.children && Utilities.isArray(child.children) && child.children.length > 0) { //recurse found = this.getDescendantNode(child, id); if (found) { @@ -10485,7 +12916,7 @@ When building a custom infinite editor view you can use the same components as a while (root === null && current) { if (current.metaData && current.metaData['treeAlias']) { root = current; - } else if (angular.isFunction(current.parent)) { + } else if (Utilities.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. @@ -10659,7 +13090,7 @@ When building a custom infinite editor view you can use the same components as a //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)) { + if (Utilities.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 @@ -10690,7 +13121,7 @@ When building a custom infinite editor view you can use the same components as a if (!node) { throw 'node cannot be null'; } - if (!angular.isFunction(node.parent)) { + if (!Utilities.isFunction(node.parent)) { throw 'node.parent is not a function, the path cannot be resolved'; } var reversePath = []; @@ -10717,7 +13148,7 @@ When building a custom infinite editor view you can use the same components as a if (!args.path) { throw 'No path defined on args object for syncTree'; } - if (!angular.isArray(args.path)) { + if (!Utilities.isArray(args.path)) { throw 'Path must be an array'; } if (args.path.length < 1) { @@ -10819,6 +13250,52 @@ When building a custom infinite editor view you can use the same components as a 'use strict'; /** * @ngdoc service + * @name umbraco.services.udiService + * @description A service for UDIs + **/ + function udiService() { + return { + /** + * @ngdoc method + * @name umbraco.services.udiService#create + * @methodOf umbraco.services.udiService + * @function + * + * @description + * Generates a Udi string with a new ID + * + * @param {string} entityType The entityType as a string. + * @returns {string} The generated UDI + */ + create: function create(entityType) { + return this.build(entityType, String.CreateGuid()); + }, + build: function build(entityType, guid) { + return 'umb://' + entityType + '/' + guid.replace(/-/g, ''); + }, + getKey: function getKey(udi) { + if (!Utilities.isString(udi)) { + throw 'udi is not a string'; + } + if (!udi.startsWith('umb://')) { + throw 'udi does not start with umb://'; + } + var withoutScheme = udi.substr('umb://'.length); + var withoutHost = withoutScheme.substr(withoutScheme.indexOf('/') + 1).trim(); + if (withoutHost.length !== 32) { + throw 'udi is not 32 chars'; + } + return ''.concat(withoutHost.substr(0, 8), '-').concat(withoutHost.substr(8, 4), '-').concat(withoutHost.substr(12, 4), '-').concat(withoutHost.substr(16, 4), '-').concat(withoutHost.substr(20)); + } + }; + } + angular.module('umbraco.services').factory('udiService', udiService); + }()); + 'use strict'; + (function () { + 'use strict'; + /** + * @ngdoc service * @name umbraco.services.udiParser * @description A object used to parse UDIs **/ @@ -10907,36 +13384,40 @@ When building a custom infinite editor view you can use the same components as a return trimmed; }, formatContentTypePostData: function formatContentTypePostData(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', 'allowCultureVariant', 'isElement'); - // TODO: Map these + // 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', 'allowCultureVariant', 'allowSegmentVariant', 'isElement'); 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 + // Do not include groups with init state return g.tabState === 'init'; }); saveModel.groups = _.map(realGroups, function (g) { - var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name'); + var saveGroup = _.pick(g, 'id', 'sortOrder', 'name', 'key', 'alias', 'type'); var realProperties = _.reject(g.properties, function (p) { - //do not include these properties + // Do not include properties with init state or inherited from a composition 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', 'isSensitiveData', 'allowCultureVariant'); + var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant', 'labelOnTop'); 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; + if (g.inherited === true) { + if (saveProperties.length === 0) { + // All properties are inherited from the compositions, no need to save this group + return null; + } else if (g.contentTypeId != saveModel.id) { + // We have local properties, but the group id is not local, ensure a new id/key is generated on save + saveGroup = _.omit(saveGroup, 'id', 'key'); + } } return saveGroup; }); - //we don't want any null groups saveModel.groups = _.reject(saveModel.groups, function (g) { + // Do not include empty/null groups return !g; }); return saveModel; @@ -10982,13 +13463,12 @@ When building a custom infinite editor view you can use the same components as a /** formats the display model used to display the user to the model used to save the user */ formatUserPostData: function formatUserPostData(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); + var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message'); //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])) { + if (!Utilities.isString(currGroups[i])) { formattedGroups.push(currGroups[i].alias); } else { formattedGroups.push(currGroups[i]); @@ -11052,7 +13532,7 @@ When building a custom infinite editor view you can use the same components as a var currSections = saveModel.sections; var formattedSections = []; for (var i = 0; i < currSections.length; i++) { - if (!angular.isString(currSections[i])) { + if (!Utilities.isString(currSections[i])) { formattedSections.push(currSections[i].alias); } else { formattedSections.push(currSections[i]); @@ -11063,7 +13543,7 @@ When building a custom infinite editor view you can use the same components as a var currUsers = saveModel.users; var formattedUsers = []; for (var j = 0; j < currUsers.length; j++) { - if (!angular.isNumber(currUsers[j])) { + if (!Utilities.isNumber(currUsers[j])) { formattedUsers.push(currUsers[j].id); } else { formattedUsers.push(currUsers[j]); @@ -11174,6 +13654,7 @@ When building a custom infinite editor view you can use the same components as a //if its null/empty,we must pass up an empty string else we get json converter errors properties: getContentProperties(v.tabs), culture: v.language ? v.language.culture : null, + segment: v.segment, publish: v.publish, save: v.save, releaseDate: v.releaseDate, @@ -11195,32 +13676,51 @@ When building a custom infinite editor view you can use the same components as a * @returns {} */ formatContentGetData: function formatContentGetData(displayModel) { - //We need to check for invariant properties among the variant variants. - //When we detect this, we want to make sure that the property object instance is the - //same reference object between all variants instead of a copy (which it will be when - //return from the JSON structure). + // We need to check for invariant properties among the variant variants, + // as the value of an invariant property is shared between different variants. + // A property can be culture invariant, segment invariant, or both. + // When we detect this, we want to make sure that the property object instance is the + // same reference object between all variants instead of a copy (which it will be when + // return from the JSON structure). if (displayModel.variants && displayModel.variants.length > 1) { - var invariantProperties = []; - //collect all invariant properties on the first first variant - var firstVariant = displayModel.variants[0]; - _.each(firstVariant.tabs, function (tab, tabIndex) { - _.each(tab.properties, function (property, propIndex) { - //in theory if there's more than 1 variant, that means they would all have a language - //but we'll do our safety checks anyways here - if (firstVariant.language && !property.culture) { - invariantProperties.push({ - tabIndex: tabIndex, - propIndex: propIndex, - property: property - }); - } - }); + // Collect all invariant properties from the variants that are either the + // default language variant or the default segment variant. + var defaultVariants = _.filter(displayModel.variants, function (variant) { + var isDefaultLanguage = variant.language && variant.language.isDefault; + var isDefaultSegment = variant.segment == null; + return isDefaultLanguage || isDefaultSegment; }); - //now assign this same invariant property instance to the same index of the other variants property array - for (var j = 1; j < displayModel.variants.length; j++) { - var variant = displayModel.variants[j]; - _.each(invariantProperties, function (invProp) { - variant.tabs[invProp.tabIndex].properties[invProp.propIndex] = invProp.property; + if (defaultVariants.length > 0) { + _.each(defaultVariants, function (defaultVariant) { + var invariantProps = []; + _.each(defaultVariant.tabs, function (tab, tabIndex) { + _.each(tab.properties, function (property, propIndex) { + // culture == null -> property is culture invariant + // segment == null -> property is *possibly* segment invariant + if (!property.culture || !property.segment) { + invariantProps.push({ + tabIndex: tabIndex, + propIndex: propIndex, + property: property + }); + } + }); + }); + var otherVariants = _.filter(displayModel.variants, function (variant) { + return variant !== defaultVariant; + }); + // now assign this same invariant property instance to the same index of the other variants property array + _.each(otherVariants, function (variant) { + _.each(invariantProps, function (invProp) { + var tab = variant.tabs[invProp.tabIndex]; + var prop = tab.properties[invProp.propIndex]; + var inheritsCulture = prop.culture === invProp.property.culture && prop.segment == null && invProp.property.segment == null; + var inheritsSegment = prop.segment === invProp.property.segment && !prop.culture; + if (inheritsCulture || inheritsSegment) { + tab.properties[invProp.propIndex] = invProp.property; + } + }); + }); }); } } @@ -11274,7 +13774,7 @@ When building a custom infinite editor view you can use the same components as a * * @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 convertVirtualToAbsolutePath(virtualPath) { @@ -11297,11 +13797,11 @@ When building a custom infinite editor view you can use the same components as a * * @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 dictionaryToQueryString(queryStrings) { - if (angular.isArray(queryStrings)) { + if (Utilities.isArray(queryStrings)) { return _.map(queryStrings, function (item) { var key = null; var val = null; @@ -11315,7 +13815,7 @@ When building a custom infinite editor view you can use the same components as a } return encodeURIComponent(key) + '=' + encodeURIComponent(val); }).join('&'); - } else if (angular.isObject(queryStrings)) { + } else if (Utilities.isObject(queryStrings)) { //this allows for a normal object to be passed in (ie. a dictionary) return decodeURIComponent($.param(queryStrings)); } @@ -11329,9 +13829,9 @@ When building a custom infinite editor view you can use the same components as a * * @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 {string} actionName The webapi action name * @param {object} queryStrings Can be either a string or an array containing key/value pairs */ getApiUrl: function getApiUrl(apiName, actionName, queryStrings) { @@ -11341,7 +13841,7 @@ When building a custom infinite editor view you can use the same components as a 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))); + return Umbraco.Sys.ServerVariables['umbracoUrls'][apiName] + actionName + (!queryStrings ? '' : '?' + (Utilities.isString(queryStrings) ? queryStrings : this.dictionaryToQueryString(queryStrings))); }, /** * @ngdoc function @@ -11351,7 +13851,7 @@ When building a custom infinite editor view you can use the same components as a * * @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 + * 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 @@ -11374,7 +13874,7 @@ When building a custom infinite editor view you can use the same components as a function defaultError(data, status, headers, config) { var err = { //NOTE: the default error message here should never be used based on the above docs! - errorMsg: angular.isString(opts) ? opts : 'An error occurred!', + errorMsg: Utilities.isString(opts) ? opts : 'An error occurred!', data: data, status: status }; @@ -11390,7 +13890,7 @@ When building a custom infinite editor view you can use the same components as a error: !opts || !opts.error ? defaultError : opts.error }; return httpPromise.then(function (response) { - //invoke the callback + //invoke the callback var result = callbacks.success.apply(this, [ response.data, response.status, @@ -11404,9 +13904,9 @@ When building a custom infinite editor view you can use the same components as a if (!response) { return; //sometimes oddly this happens, nothing we can do } - if (!response.status && response.message && response.stack) { - //this is a JS/angular error that we should deal with - return $q.reject({ errorMsg: response.message }); + if (!response.status) { + //this is a JS/angular error + return $q.reject(response); } //invoke the callback var result = callbacks.error.apply(this, [ @@ -11420,13 +13920,13 @@ When building a custom infinite editor view you can use the same components as a //show a ysod dialog if (Umbraco.Sys.ServerVariables['isDebuggingEnabled'] === true) { var error = { - errorMsg: 'An error occured', + errorMsg: 'An error occurred', data: response.data }; // TODO: All YSOD handling should be done with an interceptor overlayService.ysod(error); } else { - //show a simple error notification + //show a simple error notification notificationsService.error('Server error', 'Contact administrator, see log for full details.
      ' + result.errorMsg + ''); } } else { @@ -11447,7 +13947,7 @@ When building a custom infinite editor view you can use the same components as a * * @description * Used for saving content/media/members specifically - * + * * @param {Object} args arguments object * @returns {Promise} http promise object. */ @@ -11485,18 +13985,21 @@ When building a custom infinite editor view you can use the same components as a 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 - var fileKey = 'file_' + args.files[f].alias + '_' + (args.files[f].culture ? args.files[f].culture : ''); - if (angular.isArray(args.files[f].metaData) && args.files[f].metaData.length > 0) { - fileKey += '_' + args.files[f].metaData.join('_'); + var file = args.files[f]; + var fileKey = 'file_' + (file.alias || '').replace(/_/g, '\\_') + '_' + (file.culture ? file.culture.replace(/_/g, '\\_') : '') + '_' + (file.segment ? file.segment.replace(/_/g, '\\_') : ''); + if (Utilities.isArray(file.metaData) && file.metaData.length > 0) { + fileKey += '_' + _.map(file.metaData, function (x) { + return ('' + x).replace(/_/g, '\\_'); + }).join('_'); } - formData.append(fileKey, args.files[f].file); + formData.append(fileKey, file.file); } }).then(function (response) { //success callback //reset the tabs and set the active one if (response.data.tabs && response.data.tabs.length > 0) { - _.each(response.data.tabs, function (item) { - item.active = false; + response.data.tabs.forEach(function (item) { + return item.active = false; }); response.data.tabs[activeTabIndex].active = true; } @@ -11507,7 +14010,10 @@ When building a custom infinite editor view you can use the same components as a //the data returned is the up-to-date data so the UI will refresh return $q.resolve(response.data); }, function (response) { - //failure callback + if (!response.status) { + //this is a JS/angular error + return $q.reject(response); + } //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. if (response.status >= 500 && response.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, @@ -11518,13 +14024,13 @@ When building a custom infinite editor view you can use the same components as a } else if (Umbraco.Sys.ServerVariables['isDebuggingEnabled'] === true) { //show a ysod dialog var error = { - errorMsg: 'An error occured', + errorMsg: 'An error occurred', data: response.data }; // TODO: All YSOD handling should be done with an interceptor overlayService.ysod(error); } else { - //show a simple error notification + //show a simple error notification notificationsService.error('Server error', 'Contact administrator, see log for full details.
      ' + response.data.ExceptionMessage + ''); } } else if (args.showNotifications) { @@ -11544,8 +14050,8 @@ When building a custom infinite editor view you can use the same components as a if (!jsonData) { throw 'jsonData cannot be null'; } - if (angular.isArray(jsonData)) { - _.each(jsonData, function (item) { + if (Utilities.isArray(jsonData)) { + jsonData.forEach(function (item) { if (!item.key || !item.value) { throw 'jsonData array item must have both a key and a value property'; } @@ -11564,12 +14070,12 @@ When building a custom infinite editor view you can use the same components as a transformRequest: function transformRequest(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); + if (Utilities.isArray(data)) { + data.forEach(function (item) { + formData.append(item.key, !Utilities.isString(item.value) ? Utilities.toJson(item.value) : item.value); }); } else { - formData.append(data.key, !angular.isString(data.value) ? angular.toJson(data.value) : data.value); + formData.append(data.key, !Utilities.isString(data.value) ? Utilities.toJson(data.value) : data.value); } //call the callback if (transformCallback) { @@ -11588,11 +14094,21 @@ When building a custom infinite editor view you can use the same components as a }); }, /** + * @ngdoc method + * @name umbraco.resources.contentResource#downloadFile + * @methodOf umbraco.resources.contentResource + * + * @description * Downloads a file to the client using AJAX/XHR - * Based on an implementation here: web.student.tuwien.ac.at/~e0427417/jsdownload.html - * See https://stackoverflow.com/a/24129082/694494 + * + * @param {string} httpPath the path (url) to the resource being downloaded + * @returns {Promise} http promise object. */ downloadFile: function downloadFile(httpPath) { + /** + * Based on an implementation here: web.student.tuwien.ac.at/~e0427417/jsdownload.html + * See https://stackoverflow.com/a/24129082/694494 + */ // Use an arraybuffer return $http.get(httpPath, { responseType: 'arraybuffer' }).then(function (response) { var octetStreamMime = 'application/octet-stream'; @@ -11666,7 +14182,7 @@ When building a custom infinite editor view you can use the same components as a // Fallback to window.open method window.open(httpPath, '_blank', ''); } - return $q.resolve(); + return $q.resolve(response); }, function (response) { return $q.reject({ errorMsg: 'An error occurred downloading the file', @@ -11777,7 +14293,7 @@ When building a custom infinite editor view you can use the same components as a angular.module('umbraco.services').factory('urlHelper', urlHelper); }()); 'use strict'; - angular.module('umbraco.services').factory('userService', function ($rootScope, eventsService, $q, $location, requestRetryQueue, authResource, emailMarketingResource, $timeout, angularHelper) { + angular.module('umbraco.services').factory('userService', function ($rootScope, eventsService, $q, $location, $window, requestRetryQueue, authResource, emailMarketingResource, $timeout, angularHelper) { var currentUser = null; var lastUserId = null; //this tracks the last date/time that the user's remainingAuthSeconds was updated from the server @@ -11881,7 +14397,7 @@ When building a custom infinite editor view you can use the same components as a /** Called to update the current user's timeout */ function setUserTimeoutInternal(newTimeout) { var asNumber = parseFloat(newTimeout); - if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) { + if (!isNaN(asNumber) && currentUser && Utilities.isNumber(asNumber)) { currentUser.remainingAuthSeconds = newTimeout; lastServerTimeoutSet = new Date(); } @@ -11926,7 +14442,26 @@ When building a custom infinite editor view you can use the same components as a }, /** Returns a promise, sends a request to the server to validate the credentials */ authenticate: function authenticate(login, password) { - return authResource.performLogin(login, password).then(this.setAuthenticationSuccessful); + return authResource.performLogin(login, password).then(function (data) { + // Check if user has a start node set. + if (data.startContentIds.length === 0 && data.startMediaIds.length === 0) { + var errorMsg = 'User has no start-nodes'; + var result = { + errorMsg: errorMsg, + user: data, + authenticated: false, + lastUserId: lastUserId, + loginType: 'credentials' + }; + eventsService.emit('app.notAuthenticated', result); + // TODO: How does this make sense? How can you throw from a promise? Does this get caught by the rejection? + // If so then return $q.reject should be used. + throw result; + } + return data; + }, function (err) { + return $q.reject(err); + }).then(this.setAuthenticationSuccessful); }, setAuthenticationSuccessful: function setAuthenticationSuccessful(data) { //when it's successful, return the user data @@ -11946,8 +14481,12 @@ When building a custom infinite editor view you can use the same components as a logout: function logout() { return authResource.performLogout().then(function (data) { userAuthExpired(); - //done! - return null; + if (data && data.signOutRedirectUrl) { + $window.location.replace(data.signOutRedirectUrl); + } else { + //done! + return null; + } }); }, /** Refreshes the current user data with the data stored for the user on the server and returns it */ @@ -11962,9 +14501,9 @@ When building a custom infinite editor view you can use the same components as a }; setCurrentUser(data); deferred.resolve(currentUser); - }, function () { + }, function (err) { //it failed, so they are not logged in - deferred.reject(); + deferred.reject(err); }); return deferred.promise; }, @@ -11984,9 +14523,9 @@ When building a custom infinite editor view you can use the same components as a } setCurrentUser(data); return $q.when(currentUser); - }, function () { + }, function (err) { //it failed, so they are not logged in - return $q.reject(currentUser); + return $q.reject(err); }); } else { return $q.when(currentUser); @@ -12042,11 +14581,11 @@ When building a custom infinite editor view you can use the same components as a 'color': 'warning' } ]; - localizationService.localizeMany(_.map(userStates, function (userState) { + localizationService.localizeMany(userStates.map(function (userState) { return 'user_state' + userState.key; })).then(function (data) { var reg = /^\[[\S\s]*]$/g; - _.each(data, function (value, index) { + data.forEach(function (value, index) { if (!reg.test(value)) { // Only translate if key exists userStates[index].name = value; @@ -12054,27 +14593,19 @@ When building a custom infinite editor view you can use the same components as a }); }); function getUserStateFromValue(value) { - var foundUserState; - angular.forEach(userStates, function (userState) { - if (userState.value === value) { - foundUserState = userState; - } + return userStates.find(function (userState) { + return userState.value === value; }); - return foundUserState; } function getUserStateByKey(key) { - var foundUserState; - angular.forEach(userStates, function (userState) { - if (userState.key === key) { - foundUserState = userState; - } + return userStates.find(function (userState) { + return userState.key === key; }); - return foundUserState; } function getUserStatesFilter(userStatesObject) { var userStatesFilter = []; for (var key in userStatesObject) { - if (userStatesObject.hasOwnProperty(key)) { + if (hasOwnProperty.call(userStatesObject, key)) { var userState = getUserStateByKey(key); if (userState) { userState.count = userStatesObject[key]; @@ -12229,9 +14760,9 @@ When building a custom infinite editor view you can use the same components as a 'parentId', 'path' ]; - _.each(required, function (k) { - if (!_.has(source, k)) { - throw 'The source object does not contain the property ' + k; + required.forEach(function (k) { + if (!hasOwnProperty.call(source, k)) { + throw 'The source object does not contain the property '.concat(k); } }); var optional = [ @@ -12261,10 +14792,10 @@ When building a custom infinite editor view you can use the same components as a var storage = $window['sessionStorage'] ? $window['sessionStorage'] : {}; return { get: function get(key) { - return angular.fromJson(storage['umb_' + key]); + return Utilities.fromJson(storage['umb_' + key]); }, set: function set(key, value) { - storage['umb_' + key] = angular.toJson(value); + storage['umb_' + key] = Utilities.toJson(value); } }; } @@ -12373,7 +14904,7 @@ When building a custom infinite editor view you can use the same components as a * */ function windowResizeListener($rootScope) { - var WinReszier = function () { + var WinResizer = function () { var registered = []; var inited = false; var resize = _.debounce(function (ev) { @@ -12414,14 +14945,14 @@ When building a custom infinite editor view you can use the same components as a * @param {Function} cb */ register: function register(cb) { - WinReszier.register(cb); + WinResizer.register(cb); }, /** * Removes a registered callback * @param {Function} cb */ unregister: function unregister(cb) { - WinReszier.unregister(cb); + WinResizer.unregister(cb); } }; } @@ -12747,4 +15278,161 @@ When building a custom infinite editor view you can use the same components as a }; } angular.module('umbraco.services').factory('xmlhelper', xmlhelper); + 'use strict'; + function _typeof(obj) { + if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') { + _typeof = function _typeof(obj) { + return typeof obj; + }; + } else { + _typeof = function _typeof(obj) { + return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj; + }; + } + return _typeof(obj); + } + /** + * A friendly utility collection to replace AngularJs' ng-functions + * If it doesn't exist here, it's probably available as vanilla JS + * + * Still carries a dependency on underscore, but if usages of underscore from + * elsewhere in the codebase can instead use these methods, the underscore + * dependency will be nicely abstracted and can be removed/swapped later + * + * This collection is open to extension... + */ + (function (window) { + /** + * Equivalent to angular.noop + */ + var noop = function noop() { + }; + /** + * Facade to angular.copy + */ + var copy = function copy(src, dst) { + return angular.copy(src, dst); + }; + /** + * Equivalent to angular.isArray + */ + var isArray = function isArray(val) { + return Array.isArray(val) || val instanceof Array; + }; + /** + * Facade to angular.equals + */ + var equals = function equals(a, b) { + return angular.equals(a, b); + }; + /** + * Facade to angular.extend + * Use this with Angular objects, for vanilla JS objects, use Object.assign() + * This is an alias as it to allow passing an unknown number of arguments + */ + var extend = angular.extend; + /** + * Equivalent to angular.isFunction + */ + var isFunction = function isFunction(val) { + return typeof val === 'function'; + }; + /** + * Equivalent to angular.isUndefined + */ + var isUndefined = function isUndefined(val) { + return typeof val === 'undefined'; + }; + /** + * Equivalent to angular.isDefined. Inverts result of const isUndefined + */ + var isDefined = function isDefined(val) { + return !isUndefined(val); + }; + /** + * Equivalent to angular.isString + */ + var isString = function isString(val) { + return typeof val === 'string'; + }; + /** + * Equivalent to angular.isNumber + */ + var isNumber = function isNumber(val) { + return typeof val === 'number'; + }; + /** + * Equivalent to angular.isObject + */ + var isObject = function isObject(val) { + return val !== null && _typeof(val) === 'object'; + }; + var isWindow = function isWindow(obj) { + return obj && obj.window === obj; + }; + var isScope = function isScope(obj) { + return obj && obj.$evalAsync && obj.$watch; + }; + var toJsonReplacer = function toJsonReplacer(key, value) { + var val = value; + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { + val = undefined; + } else if (isWindow(value)) { + val = '$WINDOW'; + } else if (value && window.document === value) { + val = '$DOCUMENT'; + } else if (isScope(value)) { + val = '$SCOPE'; + } + return val; + }; + /** + * Equivalent to angular.toJson + */ + var toJson = function toJson(obj, pretty) { + if (isUndefined(obj)) + return undefined; + if (!isNumber(pretty)) { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); + }; + /** + * Equivalent to angular.fromJson + */ + var fromJson = function fromJson(val) { + if (!isString(val)) { + return val; + } + return JSON.parse(val); + }; + /** + * Not equivalent to angular.forEach. But like the angularJS method this does not fail on null or undefined. + */ + var forEach = function forEach(obj, iterator) { + if (obj && isArray(obj)) { + return obj.forEach(iterator); + } + return obj; + }; + var _utilities = { + noop: noop, + copy: copy, + isArray: isArray, + equals: equals, + extend: extend, + isFunction: isFunction, + isUndefined: isUndefined, + isDefined: isDefined, + isString: isString, + isNumber: isNumber, + isObject: isObject, + fromJson: fromJson, + toJson: toJson, + forEach: forEach + }; + if (typeof window.Utilities === 'undefined') { + window.Utilities = _utilities; + } + }(window)); }()); \ No newline at end of file diff --git a/TestSite/Umbraco/Js/umbraco.websitepreview.js b/TestSite/Umbraco/Js/umbraco.websitepreview.js new file mode 100644 index 0000000..664e9f7 --- /dev/null +++ b/TestSite/Umbraco/Js/umbraco.websitepreview.js @@ -0,0 +1,121 @@ +(function () { + 'use strict'; + /*********************************************************************************************************/ + /* website preview */ + /*********************************************************************************************************/ + (function () { + if (window.location !== window.parent.location) { + //we are in an iFrame, so lets skip the dialog. + return; + } + var scriptElement = document.currentScript; + function setCookie(cname, cvalue, exminutes) { + var d = new Date(); + d.setTime(d.getTime() + exminutes * 60 * 1000); + document.cookie = cname + '=' + cvalue + ';expires=' + d.toUTCString() + ';path=/'; + } + function getCookie(cname) { + var name = cname + '='; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return null; + } + function beforeUnloadHandler(e) { + endPreviewSession(); + } + window.addEventListener('beforeunload', beforeUnloadHandler, false); + function startPreviewSession() { + // lets registrer this preview session. + var amountOfPreviewSessions = Math.max(localStorage.getItem('UmbPreviewSessionAmount') || 0, 0); + amountOfPreviewSessions++; + localStorage.setItem('UmbPreviewSessionAmount', amountOfPreviewSessions); + } + function resetPreviewSessions() { + localStorage.setItem('UmbPreviewSessionAmount', 0); + } + function endPreviewSession() { + var amountOfPreviewSessions = localStorage.getItem('UmbPreviewSessionAmount') || 0; + amountOfPreviewSessions--; + localStorage.setItem('UmbPreviewSessionAmount', amountOfPreviewSessions); + if (amountOfPreviewSessions <= 0) { + // We are good to secretly end preview mode. + navigator.sendBeacon(scriptElement.getAttribute('data-umbraco-path') + '/preview/end'); + } + } + startPreviewSession(); + function endPreviewMode() { + resetPreviewSessions(); + window.top.location.href = scriptElement.getAttribute('data-umbraco-path') + '/preview/end?redir=' + encodeURIComponent(window.location.pathname + window.location.search); + } + function continuePreviewMode(minutsToExpire) { + setCookie('UMB-WEBSITE-PREVIEW-ACCEPT', 'true', minutsToExpire || 4); + } + var user = getCookie('UMB-WEBSITE-PREVIEW-ACCEPT'); + if (user != 'true') { + askToViewPublishedVersion(); + } else { + continuePreviewMode(); + } + function askToViewPublishedVersion() { + scriptElement.getAttribute('data-umbraco-path'); + var request = new XMLHttpRequest(); + request.open('GET', scriptElement.getAttribute('data-umbraco-path') + '/LocalizedText'); + request.send(); + request.onreadystatechange = function (e) { + if (request.readyState == 4 && request.status == 200) { + var jsonLocalization = JSON.parse(request.responseText); + createAskUserAboutVersionDialog(jsonLocalization); + } + }; + } + function createAskUserAboutVersionDialog(jsonLocalization) { + var localizeVarsFallback = { + 'viewPublishedContentHeadline': 'Preview content?', + 'viewPublishedContentDescription': 'You have ended preview mode, do you want to continue previewing this content?', + 'viewPublishedContentAcceptButton': 'You have ended preview mode, do you want to continue previewing this content?', + 'viewPublishedContentDeclineButton': 'Preview' + }; + var umbLocalizedVars = jsonLocalization || {}; + umbLocalizedVars.preview = Object.assign(localizeVarsFallback, jsonLocalization.preview); + // This modal is also used in preview.js + var modelStyles = '\n /* Webfont: LatoLatin-Bold */\n @font-face {\n font-family: \'Lato\';\n src: url(\'https://fonts.googleapis.com/css2?family=Lato:wght@700&display=swap\');\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n text-rendering: optimizeLegibility;\n }\n\n .umbraco-preview-dialog {\n position: fixed;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 99999999;\n top:0;\n bottom:0;\n left:0;\n right:0;\n overflow: auto;\n background-color: rgba(0,0,0,0.6);\n }\n\n .umbraco-preview-dialog__modal {\n background-color: #fff;\n border-radius: 6px;\n box-shadow: 0 3px 7px rgba(0,0,0,0.3);\n margin: auto;\n padding: 30px 40px;\n width: 100%;\n max-width: 540px;\n font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif;\n font-size: 15px;\n line-height: 1.5;\n }\n\n .umbraco-preview-dialog__headline {\n font-weight: 700;\n font-size: 22px;\n color: #1b264f;\n margin-top:10px;\n margin-bottom:20px;\n }\n .umbraco-preview-dialog__question {\n margin-bottom:30px;\n }\n .umbraco-preview-dialog__modal > button {\n display: inline-block;\n cursor: pointer;\n padding: 8px 18px;\n text-align: center;\n vertical-align: middle;\n border-radius: 3px;\n border:none;\n font-family: inherit;\n font-weight: 700;\n font-size: 15px;\n float:right;\n margin-left:10px;\n\n color: #1b264f;\n background-color: #f6f1ef;\n }\n .umbraco-preview-dialog__modal > button:hover {\n color: #2152a3;\n background-color: #f6f1ef;\n }\n .umbraco-preview-dialog__modal > button.umbraco-preview-dialog__continue {\n color: #fff;\n background-color: #2bc37c;\n }\n .umbraco-preview-dialog__modal > button.umbraco-preview-dialog__continue:hover {\n background-color: #39d38b;\n }\n '; + var bodyEl = document.getElementsByTagName('BODY')[0]; + var fragment = document.createElement('div'); + var shadowRoot = fragment.attachShadow({ mode: 'open' }); + var style = document.createElement('style'); + style.innerHTML = modelStyles; + shadowRoot.appendChild(style); + var con = document.createElement('div'); + con.className = 'umbraco-preview-dialog'; + shadowRoot.appendChild(con); + var modal = document.createElement('div'); + modal.className = 'umbraco-preview-dialog__modal'; + modal.innerHTML = '
      '.concat(umbLocalizedVars.preview.viewPublishedContentHeadline, '
      \n
      ').concat(umbLocalizedVars.preview.viewPublishedContentDescription, '
      '); + con.appendChild(modal); + var continueButton = document.createElement('button'); + continueButton.type = 'button'; + continueButton.className = 'umbraco-preview-dialog__continue'; + continueButton.innerHTML = umbLocalizedVars.preview.viewPublishedContentAcceptButton; + continueButton.addEventListener('click', endPreviewMode); + modal.appendChild(continueButton); + var exitButton = document.createElement('button'); + exitButton.type = 'button'; + exitButton.innerHTML = umbLocalizedVars.preview.viewPublishedContentDeclineButton; + exitButton.addEventListener('click', function () { + bodyEl.removeChild(fragment); + continuePreviewMode(5); + }); + modal.appendChild(exitButton); + bodyEl.appendChild(fragment); + continueButton.focus(); + } + }()); +}()); \ No newline at end of file diff --git a/TestSite/Umbraco/Js/utilities.js b/TestSite/Umbraco/Js/utilities.js new file mode 100644 index 0000000..14e37ec --- /dev/null +++ b/TestSite/Umbraco/Js/utilities.js @@ -0,0 +1,139 @@ +/** + * A friendly utility collection to replace AngularJs' ng-functions + * If it doesn't exist here, it's probably available as vanilla JS + * + * Still carries a dependency on underscore, but if usages of underscore from + * elsewhere in the codebase can instead use these methods, the underscore + * dependency will be nicely abstracted and can be removed/swapped later + * + * This collection is open to extension... + */ +(function (window) { + + /** + * Equivalent to angular.noop + */ + const noop = () => { }; + + /** + * Facade to angular.copy + */ + const copy = (src, dst) => angular.copy(src, dst); + + /** + * Equivalent to angular.isArray + */ + const isArray = val => Array.isArray(val) || val instanceof Array; + + /** + * Facade to angular.equals + */ + const equals = (a, b) => angular.equals(a, b); + + /** + * Facade to angular.extend + * Use this with Angular objects, for vanilla JS objects, use Object.assign() + * This is an alias as it to allow passing an unknown number of arguments + */ + const extend = angular.extend; + + /** + * Equivalent to angular.isFunction + */ + const isFunction = val => typeof val === 'function'; + + /** + * Equivalent to angular.isUndefined + */ + const isUndefined = val => typeof val === 'undefined'; + + /** + * Equivalent to angular.isDefined. Inverts result of const isUndefined + */ + const isDefined = val => !isUndefined(val); + + /** + * Equivalent to angular.isString + */ + const isString = val => typeof val === 'string'; + + /** + * Equivalent to angular.isNumber + */ + const isNumber = val => typeof val === 'number'; + + /** + * Equivalent to angular.isObject + */ + const isObject = val => val !== null && typeof val === 'object'; + + const isWindow = obj => obj && obj.window === obj; + + const isScope = obj => obj && obj.$evalAsync && obj.$watch; + + const toJsonReplacer = (key, value) => { + var val = value; + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { + val = undefined; + } else if (isWindow(value)) { + val = '$WINDOW'; + } else if (value && window.document === value) { + val = '$DOCUMENT'; + } else if (isScope(value)) { + val = '$SCOPE'; + } + return val; + }; + + /** + * Equivalent to angular.toJson + */ + const toJson = (obj, pretty) => { + if (isUndefined(obj)) return undefined; + if (!isNumber(pretty)) { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); + }; + + /** + * Equivalent to angular.fromJson + */ + const fromJson = (val) => { + if (!isString(val)) { + return val; + } + return JSON.parse(val); + }; + + /** + * Not equivalent to angular.forEach. But like the angularJS method this does not fail on null or undefined. + */ + const forEach = (obj, iterator) => { + if (obj && isArray(obj)) { + return obj.forEach(iterator); + } + return obj; + }; + + let _utilities = { + noop: noop, + copy: copy, + isArray: isArray, + equals: equals, + extend: extend, + isFunction: isFunction, + isUndefined: isUndefined, + isDefined: isDefined, + isString: isString, + isNumber: isNumber, + isObject: isObject, + fromJson: fromJson, + toJson: toJson, + forEach: forEach + }; + + if (typeof (window.Utilities) === 'undefined') { + window.Utilities = _utilities; + } +})(window); diff --git a/TestSite/Umbraco/PartialViewMacros/Templates/Gallery.cshtml b/TestSite/Umbraco/PartialViewMacros/Templates/Gallery.cshtml index 5aa09a3..64be8eb 100644 --- a/TestSite/Umbraco/PartialViewMacros/Templates/Gallery.cshtml +++ b/TestSite/Umbraco/PartialViewMacros/Templates/Gallery.cshtml @@ -29,13 +29,13 @@ @* a single image *@ if (media.IsDocumentType("Image")) { - @Render(media); + @Render(media) } @* a folder with images under it *@ foreach (var image in media.Children()) { - @Render(image); + @Render(image) } }
      diff --git a/TestSite/Umbraco/Views/AuthorizeUpgrade.cshtml b/TestSite/Umbraco/Views/AuthorizeUpgrade.cshtml index b7c1e65..f6f9b4b 100644 --- a/TestSite/Umbraco/Views/AuthorizeUpgrade.cshtml +++ b/TestSite/Umbraco/Views/AuthorizeUpgrade.cshtml @@ -61,7 +61,7 @@
      @@ -65,7 +87,10 @@ } - + + diff --git a/TestSite/Umbraco/Views/common/drawers/help/help.html b/TestSite/Umbraco/Views/common/drawers/help/help.html index aa6126e..82a37e6 100644 --- a/TestSite/Umbraco/Views/common/drawers/help/help.html +++ b/TestSite/Umbraco/Views/common/drawers/help/help.html @@ -2,8 +2,8 @@ + title="{{vm.title}}" + description="{{vm.subtitle}}"> @@ -14,20 +14,19 @@
      Need help editing current item '{{vm.nodeName}}' ?
      -
      -
      +
      {{ tour.name }}
      - +
      -
      +
      @@ -47,7 +46,7 @@
      ng-click="tourGroup.open = !tourGroup.open" aria-expanded="{{tourGroup.open === undefined ? false : tourGroup.open }}"> - + {{tourGroup.group}} Other @@ -98,58 +97,57 @@
      - - - -
      -
      - Videos -
      - -
      - - -
    + - - + + -
    - Visit our.umbraco.com -
    - - The friendliest community - - - + + @@ -159,6 +157,7 @@
    diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.content.html b/TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.content.html new file mode 100644 index 0000000..15c3b95 --- /dev/null +++ b/TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.content.html @@ -0,0 +1 @@ + diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.html b/TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.html new file mode 100644 index 0000000..8fe5526 --- /dev/null +++ b/TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.html @@ -0,0 +1,51 @@ +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.settings.html b/TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.settings.html new file mode 100644 index 0000000..df69e2e --- /dev/null +++ b/TestSite/Umbraco/Views/common/infiniteeditors/blockeditor/blockeditor.settings.html @@ -0,0 +1 @@ + diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/blockpicker/blockpicker.html b/TestSite/Umbraco/Views/common/infiniteeditors/blockpicker/blockpicker.html new file mode 100644 index 0000000..2735993 --- /dev/null +++ b/TestSite/Umbraco/Views/common/infiniteeditors/blockpicker/blockpicker.html @@ -0,0 +1,84 @@ +
    + + + + + + + + +
    + +
    + + +
    + +
    + + + + +
    +
    + +
    +
    + + +
    + +
    + + + + +
    +
    + +
    + + + + + + + + + + + +
    +
    diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/compositions/compositions.html b/TestSite/Umbraco/Views/common/infiniteeditors/compositions/compositions.html index 4096192..7ec6901 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/compositions/compositions.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/compositions/compositions.html @@ -17,32 +17,28 @@
    - + + + +
    - +
    - - + + - + @@ -50,10 +46,10 @@

      -
    • - +  {{contentTypeEntity.contentType.name}}
    • @@ -63,26 +59,26 @@
      • - + {{group.containerPath}}
      • - -
        - - + ng-class="{'-disabled': (compositeContentType.allowed === false && !compositeContentType.selected) || compositeContentType.inherited, '-selected': compositeContentType.selected}"> + +
        + +
        -
        diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html b/TestSite/Umbraco/Views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html new file mode 100644 index 0000000..7146d6d --- /dev/null +++ b/TestSite/Umbraco/Views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html @@ -0,0 +1,68 @@ +
        + + +
        + + + + + + + + + + +
        +
          +
        • +
          +
          +
          + +
        • +
        +
          +
        • + +
        • +
        + +
        + +
        +
        +
        + + + + + + + + +
        + +
        + +
        diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/datatypepicker/datatypepicker.html b/TestSite/Umbraco/Views/common/infiniteeditors/datatypepicker/datatypepicker.html index 534fdc5..5a3f7e9 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/datatypepicker/datatypepicker.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/datatypepicker/datatypepicker.html @@ -15,127 +15,85 @@ - -
        - -
        + + - + - -
        - - - -
        -
        -
        {{key}}
        -
          -
        • - - - - {{ systemDataType.name }} - - -
        • -
        -
        -
        -
        -
        -
        {{key}}
        -
          -
        • -
          -
          -
          - - - - {{ dataType.name }} - - -
        • -
        -
        -
        -
        +
        +
        +

        {{key | umbCmsTitleCase}}

        +
          +
        • + +
        • +
        +
        - -
        -
        -
        -
        -
        -
        {{result.group}}
        -
          -
        • -
          -
          -
          - - - - {{dataType.name}} - + +
          +

          + Available configurations +

          +
          +
          +

          {{result.group | umbCmsTitleCase}}

          +
            +
          • +
            +
            +
            +
          • -
          -
          + +
        • +
        -
        -
        -
        -
        -
        {{result.group}}
        -
          -
        • - - - - {{systemDataType.name}} - +

          + Create a new configuration +

          +
          +
          +

          {{result.group | umbCmsTitleCase}}

          +
            +
          • +
          • -
          -
          + +
        • +
        - - - -
        diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/datatypesettings/datatypesettings.html b/TestSite/Umbraco/Views/common/infiniteeditors/datatypesettings/datatypesettings.html index 8acaa54..a4fef28 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/datatypesettings/datatypesettings.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/datatypesettings/datatypesettings.html @@ -21,8 +21,8 @@ -
        - +
        + using this editor will get updated with the new settings.
        @@ -36,13 +36,15 @@
        - -
        - - - - - + +
        +
        + + + + +
        + diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/embed/embed.html b/TestSite/Umbraco/Views/common/infiniteeditors/embed/embed.html index 5862ca7..fd86f55 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/embed/embed.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/embed/embed.html @@ -15,8 +15,8 @@ - - + + -

        + +
        - - + + - - + + - - + +
        diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/iconpicker/iconpicker.html b/TestSite/Umbraco/Views/common/infiniteeditors/iconpicker/iconpicker.html index c2b0d0e..41bf92b 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/iconpicker/iconpicker.html @@ -18,17 +18,14 @@
        - + +
        @@ -45,10 +42,10 @@
          -
        • -
        diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html b/TestSite/Umbraco/Views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html index ea247c7..2ce7af8 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html @@ -9,32 +9,32 @@ hide-icon="true" hide-description="true"> - +
        -
        -
        ...
        -
        -
        -
        -
        ...
        -
        - -
        -
        -
        -
        ...
        -
        - -
        -
        -
        -
        ...
        -
        -
        + + + +
        @@ -54,5 +54,5 @@ - +
        diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/insertfield/insertfield.html b/TestSite/Umbraco/Views/common/infiniteeditors/insertfield/insertfield.html index bbb2e8c..00b1856 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/insertfield/insertfield.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/insertfield/insertfield.html @@ -71,7 +71,7 @@
        -
        {{ vm.generateOutputSample() }}
        + {{ vm.generateOutputSample() }}
        diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/itempicker/itempicker.html b/TestSite/Umbraco/Views/common/infiniteeditors/itempicker/itempicker.html index 18d0c40..cd65922 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/itempicker/itempicker.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/itempicker/itempicker.html @@ -13,26 +13,26 @@ - @@ -60,18 +57,20 @@
        {{key}}
        {{result.group}}
        @@ -79,7 +78,7 @@
        {{result.group}}
        - + Sorry, we can not find what you are looking for.
      diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/macropicker/macropicker.html b/TestSite/Umbraco/Views/common/infiniteeditors/macropicker/macropicker.html index fc1bec4..49d40ef 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/macropicker/macropicker.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/macropicker/macropicker.html @@ -15,34 +15,30 @@
      -
      - - - +
      + + + +

      + position="center"> There are no macros available to insert @@ -57,7 +53,7 @@
      {{model.selectedMacro.name}}
    • - + diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html b/TestSite/Umbraco/Views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html new file mode 100644 index 0000000..938d719 --- /dev/null +++ b/TestSite/Umbraco/Views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html @@ -0,0 +1,127 @@ +
      + + + + + + + + +
      + +
      + This item is in the Recycle Bin +
      + +
      +
      + + + + +
      + +
      + +
      + + + + + +
      + +
      + + + + + + + +
      + + + +
      +
      + +
      +
      + +
      + + + + + + + + + + + + + + + + +
      +
      +
      diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/mediapicker/mediapicker.html b/TestSite/Umbraco/Views/common/infiniteeditors/mediapicker/mediapicker.html index 917010d..052bad8 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/mediapicker/mediapicker.html @@ -1,15 +1,17 @@
      - + - +
      @@ -19,115 +21,119 @@
      - +
      -
      - +
      +
      + + +
      +
      +
      + + +
      +
      • - - + +
      • -
      • - - + +
      • -
      • - - - + + +
      -
      +
      - + - + - +
      - +
      @@ -136,66 +142,30 @@
    • + - - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      - -
      -
      - Preview -
      - - {{target.name}} -
      - -
      -
      - Focal point -
      - -
      - - -
      - -
      - -
      - Preview -
      - - - - -
      -
      - -
      - -
      + + - +
      + + +
      + +
      + + +
      @@ -223,4 +193,4 @@
      -
      +
      diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html b/TestSite/Umbraco/Views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html new file mode 100644 index 0000000..36cc3cf --- /dev/null +++ b/TestSite/Umbraco/Views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html @@ -0,0 +1,102 @@ +
      + + + + + + + +
      +
      + +
      + +
      + +
      +
      + +
      + +
      + +
      +
      + +
      + +
      + +
      +
      +
      + Preview +
      + + {{model.target.name}} +
      + +
      +
      + Focal point +
      + +
      + + +
      + +
      +
      +
      + +
      +
      + Crop section +
      + +
      + + +
      +
      + +
      + +
      + + + + + + + + + + + + + +
      +
      diff --git a/TestSite/Umbraco/Views/common/infiniteeditors/propertysettings/propertysettings.html b/TestSite/Umbraco/Views/common/infiniteeditors/propertysettings/propertysettings.html index 4474390..8379062 100644 --- a/TestSite/Umbraco/Views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/TestSite/Umbraco/Views/common/infiniteeditors/propertysettings/propertysettings.html @@ -18,8 +18,8 @@
      -
      -
      +
      +
      - +
      - - - - - -
      - - - - - -
      - {{ model.property.dataTypeName }} - {{ model.property.editor }} + + + +
      + + + +
      - +
      - +
      - - + on-click="vm.toggleValidation()" + label-on="{{vm.labels.fieldIsMandatory}}" + label-off="{{vm.labels.fieldIsMandatory}}" + show-labels="true" + label-position="right" focus-when="{{vm.focusOnMandatoryField}}" + class="mb1">
    placeholder="@validation_mandatoryMessage" ng-model="model.property.validation.mandatoryMessage" ng-if="model.property.validation.mandatory" - ng-keypress="vm.submitOnEnter($event)"> - + ng-keypress="vm.submitOnEnter($event)" />