I've created a custom section in Umbraco 7 that references external urls, but have a requirement to extend it to use exactly the same functionality as the media picker from the 'Content' rich text editor. I don't need any other rich text functionality other than to load the media picker overlay from an icon, and select either an internal or external url.
I've tried to distil the umbraco source code, as well as trying various adaptations of online tutorials, but as yet I can't get the media picker to load.
I know that fundamentally I need:
Another angular controller to return the data from the content
'getall' method
An html section that contains the media picker overlay
A reference in the edit.html in my custom section to launch the overlay.
However, as yet I haven't been able to wire it all together, so any help much appreciated.
So, this is how I came up with the solution.....
The first win was that I discovered 2 excellent tutorial blog posts, upon the shoulders of which this solution stands, so much respect to the following code cats:
Tim Geyssons - Nibble postings:
http://www.nibble.be/?p=440
Markus Johansson - Enkelmedia
http://www.enkelmedia.se/blogg/2013/11/22/creating-custom-sections-in-umbraco-7-part-1.aspx
Create a model object to represent a keyphrase, which will be associated to a new, simple, ORM table.
The ToString() method allows a friendly name to be output on the front-end.
[TableName("Keyphrase")]
public class Keyphrase
{
[PrimaryKeyColumn(AutoIncrement = true)]
public int Id { get; set; }
public string Name { get; set; }
public string Phrase { get; set; }
public string Link { get; set; }
public override string ToString()
{
return Name;
}
}
Create an Umbraco 'application' that will register the new custom section by implementing the IApplication interface. I've called mine 'Utilities' and associated it to the utilities icon.
[Application("Utilities", "Utilities", "icon-utilities", 8)]
public class UtilitiesApplication : IApplication { }
The decorator allows us to supply a name, alias, icon and sort-order of the new custom section.
Create an Umbraco tree web controller that will allow us to create the desired menu behaviour for our keyphrases, and display the keyphrase collection from our database keyphrase table.
[PluginController("Utilities")]
[Umbraco.Web.Trees.Tree("Utilities", "KeyphraseTree", "Keyphrase", iconClosed: "icon-doc", sortOrder: 1)]
public class KeyphraseTreeController : TreeController
{
private KeyphraseApiController _keyphraseApiController;
public KeyphraseTreeController()
{
_keyphraseApiController = new KeyphraseApiController();
}
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
{
var nodes = new TreeNodeCollection();
var keyphrases = _keyphraseApiController.GetAll();
if (id == Constants.System.Root.ToInvariantString())
{
foreach (var keyphrase in keyphrases)
{
var node = CreateTreeNode(
keyphrase.Id.ToString(),
"-1",
queryStrings,
keyphrase.ToString(),
"icon-book-alt",
false);
nodes.Add(node);
}
}
return nodes;
}
protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
{
var menu = new MenuItemCollection();
if (id == Constants.System.Root.ToInvariantString())
{
// root actions
menu.Items.Add<CreateChildEntity, ActionNew>(ui.Text("actions", ActionNew.Instance.Alias));
menu.Items.Add<RefreshNode, ActionRefresh>(ui.Text("actions", ActionRefresh.Instance.Alias), true);
return menu;
}
else
{
menu.Items.Add<ActionDelete>(ui.Text("actions", ActionDelete.Instance.Alias));
}
return menu;
}
}
The class decorators and TreeController extension allow us to declare the web controller for our keyphrase tree, associate it to our Utilities custom section, as well as choose an icon and sort order.
We also declare an api controller (we'll get to that!), which will allow us access to our Keyphrase data object.
The GetTreeNodes method allows us to iterate the keyphrase data collection and return the resultant nodes to the view.
The GetMenuNode method allows us to create the menu options we require for our custom section.
We state that if the node is the root (Utilities), then allow us to add child nodes and refresh the node collection.
However, if we are lower in the node tree (Keyphrase) then we only want users to be able to delete the node (ie the user shouldn't be allowed to create another level of nodes deeper than Keyphrase)
Create an api controller for our Keyphrase CRUD requests
public class KeyphraseApiController : UmbracoAuthorizedJsonController
{
public IEnumerable<Keyphrase> GetAll()
{
var query = new Sql().Select("*").From("keyphrase");
return DatabaseContext.Database.Fetch<Keyphrase>(query);
}
public Keyphrase GetById(int id)
{
var query = new Sql().Select("*").From("keyphrase").Where<Keyphrase>(x => x.Id == id);
return DatabaseContext.Database.Fetch<Keyphrase>(query).FirstOrDefault();
}
public Keyphrase PostSave(Keyphrase keyphrase)
{
if (keyphrase.Id > 0)
DatabaseContext.Database.Update(keyphrase);
else
DatabaseContext.Database.Save(keyphrase);
return keyphrase;
}
public int DeleteById(int id)
{
return DatabaseContext.Database.Delete<Keyphrase>(id);
}
}
Create the custom section views with angular controllers, which is the current architectual style in Umbraco 7.
It should be noted that Umbraco expects that your custom section components are put into the following structure App_Plugins//BackOffice/
We need a view to display and edit our keyphrase name, target phrase and url
<form name="keyphraseForm"
ng-controller="Keyphrase.KeyphraseEditController"
ng-show="loaded"
ng-submit="save(keyphrase)"
val-form-manager>
<umb-panel>
<umb-header>
<div class="span7">
<umb-content-name placeholder=""
ng-model="keyphrase.Name" />
</div>
<div class="span5">
<div class="btn-toolbar pull-right umb-btn-toolbar">
<umb-options-menu ng-show="currentNode"
current-node="currentNode"
current-section="{{currentSection}}">
</umb-options-menu>
</div>
</div>
</umb-header>
<div class="umb-panel-body umb-scrollable row-fluid">
<div class="tab-content form-horizontal" style="padding-bottom: 90px">
<div class="umb-pane">
<umb-control-group label="Target keyphrase" description="Keyphrase to be linked'">
<input type="text" class="umb-editor umb-textstring" ng-model="keyphrase.Phrase" required />
</umb-control-group>
<umb-control-group label="Keyphrase link" description="Internal or external url">
<p>{{keyphrase.Link}}</p>
<umb-link-picker ng-model="keyphrase.Link" required/>
</umb-control-group>
<div class="umb-tab-buttons" detect-fold>
<div class="btn-group">
<button type="submit" data-hotkey="ctrl+s" class="btn btn-success">
<localize key="buttons_save">Save</localize>
</button>
</div>
</div>
</div>
</div>
</div>
</umb-panel>
</form>
This utilises umbraco and angular markup to display data input fields dynamically and associate our view to an angular controller that interacts with our data layer
angular.module("umbraco").controller("Keyphrase.KeyphraseEditController",
function ($scope, $routeParams, keyphraseResource, notificationsService, navigationService) {
$scope.loaded = false;
if ($routeParams.id == -1) {
$scope.keyphrase = {};
$scope.loaded = true;
}
else {
//get a keyphrase id -> service
keyphraseResource.getById($routeParams.id).then(function (response) {
$scope.keyphrase = response.data;
$scope.loaded = true;
});
}
$scope.save = function (keyphrase) {
keyphraseResource.save(keyphrase).then(function (response) {
$scope.keyphrase = response.data;
$scope.keyphraseForm.$dirty = false;
navigationService.syncTree({ tree: 'KeyphraseTree', path: [-1, -1], forceReload: true });
notificationsService.success("Success", keyphrase.Name + " has been saved");
});
};
});
Then we need html and corresponding angular controller for the keyphrase delete behaviour
<div class="umb-pane" ng-controller="Keyphrase.KeyphraseDeleteController">
<p>
Are you sure you want to delete {{currentNode.name}} ?
</p>
<div>
<div class="umb-pane btn-toolbar umb-btn-toolbar">
<div class="control-group umb-control-group">
<a href="" class="btn btn-link" ng-click="cancelDelete()"
<localize key="general_cancel">Cancel</localize>
</a>
<a href="" class="btn btn-primary" ng-click="delete(currentNode.id)">
<localize key="general_ok">OK</localize>
</a>
</div>
</div>
</div>
</div>
Utilise Umbraco's linkpicker to allow a user to select an internal or external url.
We need html markup to launch the LinkPicker
<div>
<ul class="unstyled list-icons">
<li>
<i class="icon icon-add blue"></i>
<a href ng-click="openLinkPicker()" prevent-default>Select</a>
</li>
</ul>
</div>
And an associated directive js file that launches the link picker and posts the selected url back to the html view
angular.module("umbraco.directives")
.directive('umbLinkPicker', function (dialogService, entityResource) {
return {
restrict: 'E',
replace: true,
templateUrl: '/App_Plugins/Utilities/umb-link-picker.html',
require: "ngModel",
link: function (scope, element, attr, ctrl) {
ctrl.$render = function () {
var val = parseInt(ctrl.$viewValue);
if (!isNaN(val) && angular.isNumber(val) && val > 0) {
entityResource.getById(val, "Content").then(function (item) {
scope.node = item;
});
}
};
scope.openLinkPicker = function () {
dialogService.linkPicker({ callback: populateLink });
}
scope.removeLink = function () {
scope.node = undefined;
updateModel(0);
}
function populateLink(item) {
scope.node = item;
updateModel(item.url);
}
function updateModel(id) {
ctrl.$setViewValue(id);
}
}
};
});
There is one final js file that allows us to send data across the wire, with everyone's favourite http verbs GET, POST(handles put too here too) and DELETE
angular.module("umbraco.resources")
.factory("keyphraseResource", function ($http) {
return {
getById: function (id) {
return $http.get("BackOffice/Api/KeyphraseApi/GetById?id=" + id);
},
save: function (keyphrase) {
return $http.post("BackOffice/Api/KeyphraseApi/PostSave", angular.toJson(keyphrase));
},
deleteById: function (id) {
return $http.delete("BackOffice/Api/KeyphraseApi/DeleteById?id=" + id);
}
};
});
In addition, we will need a package manifest to register our javascript behaviour
{
javascript: [
'~/App_Plugins/Utilities/BackOffice/KeyphraseTree/edit.controller.js',
'~/App_Plugins/Utilities/BackOffice/KeyphraseTree/delete.controller.js',
'~/App_Plugins/Utilities/keyphrase.resource.js',
'~/App_Plugins/Utilities/umbLinkPicker.directive.js'
]
}
Implement tweaks to allow the CMS portion of the solution to work correctly.
At this point we've almost got our custom section singing, but we just need to jump a couple more Umbraco hoops, namely
a) add a keyphrase event class that creates our keyphrase db table if it doesn't exist (see point 8)
b) fire up Umbraco and associate the new custom section to the target user (from the User menu)
c) alter the placeholder text for the custom section by searching for it in umbraco-->config-->en.xml and swapping out the placeholder text for 'Utilities'
Intercept target content fields of target datatypes when content is saved or published
The requirement I was given was to intercept the body content of a news article, so you'll need to create a document type in Umbraco that has, for example, a title field of type 'Textstring', and bodyContent field of type 'Richtext editor'.
You'll also want a, or many, keyphrase(s) to target, which should now be in a new Umbraco custom section, 'Utilities'
Here I've targeted the keyphrase 'technology news' to link to the bbc technology news site so that any time I write the phrase 'technology news' the href link will be inserted automatically.
This is obviously quite a simple example, but would be quite powerful if a user needed to link to certain repetitive legal documents, for example tax, property, due dilligence, for example, which could be hosted either externally or within the CMS itself. The href link will open an external resource in a new tab, and internal resource in the same window (we'll get to that in Point 9)
So, the principle of what we're trying to achieve is to intercept the Umbraco save event for a document and manipulate our rich text to insert our link. This is done as follows:
a) Establish a method (ContentServiceOnSaving) that will fire when a user clicks 'save', or 'publish and save'.
b) Target our desired content field to find our keyphrases.
c) Parse the target content html against our keyphrase collection to create our internal/external links.
NB: If you just want to get the custom section up and running, you only need the ApplicationStarted method to create the KeyPhrase table.
public class KeyphraseEvents : ApplicationEventHandler
{
private KeyphraseApiController _keyphraseApiController;
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication,
ApplicationContext applicationContext)
{
_keyphraseApiController = new KeyphraseApiController();
ContentService.Saving += ContentServiceOnSaving;
var db = applicationContext.DatabaseContext.Database;
if (!db.TableExist("keyphrase"))
{
db.CreateTable<Keyphrase>(false);
}
}
private void ContentServiceOnSaving(IContentService sender, SaveEventArgs<IContent> saveEventArgs)
{
var keyphrases = _keyphraseApiController.GetAll();
var keyphraseContentParser = new KeyphraseContentParser();
foreach (IContent content in saveEventArgs.SavedEntities)
{
if (content.ContentType.Alias.Equals("NewsArticle"))
{
var blogContent = content.GetValue<string>("bodyContent");
var parsedBodyText = keyphraseContentParser.ReplaceKeyphrasesWithLinks(blogContent, keyphrases);
content.SetValue("bodyContent", parsedBodyText);
}
}
}
}
The ContentServiceOnSaving method allows us to intercept any save event in Umbraco. Afterwhich we check our incoming content to see if it's of the type we're expecting - in this example 'NewsArticle' - and if it is, then target the 'bodyContent' section, parse this with our 'KeyphraseContentParser', and swap the current 'bodyContent' with the parsed 'bodyContent'.
Create a Keyphrase parser to swap keyphrases for internal/external links
public class KeyphraseContentParser
{
public string ReplaceKeyphrasesWithLinks(string htmlContent, IEnumerable<Keyphrase> keyphrases)
{
var parsedHtmlStringBuilder = new StringBuilder(htmlContent);
foreach (var keyphrase in keyphrases)
{
if (htmlContent.CaseContains(keyphrase.Phrase, StringComparison.OrdinalIgnoreCase))
{
var index = 0;
do
{
index = parsedHtmlStringBuilder.ToString()
.IndexOf(keyphrase.Phrase, index, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
var keyphraseSuffix = parsedHtmlStringBuilder.ToString(index, keyphrase.Phrase.Length + 4);
var keyPhraseFromContent = parsedHtmlStringBuilder.ToString(index, keyphrase.Phrase.Length);
var keyphraseTarget = "_blank";
if (keyphrase.Link.StartsWith("/"))
{
keyphraseTarget = "_self";
}
var keyphraseLinkReplacement = String.Format("<a href='{0}' target='{1}'>{2}</a>",
keyphrase.Link, keyphraseTarget, keyPhraseFromContent);
if (!keyphraseSuffix.Equals(String.Format("{0}</a>", keyPhraseFromContent)))
{
parsedHtmlStringBuilder.Remove(index, keyPhraseFromContent.Length);
parsedHtmlStringBuilder.Insert(index, keyphraseLinkReplacement);
index += keyphraseLinkReplacement.Length;
}
else
{
var previousStartBracket = parsedHtmlStringBuilder.ToString().LastIndexOf("<a", index);
var nextEndBracket = parsedHtmlStringBuilder.ToString().IndexOf("a>", index);
parsedHtmlStringBuilder.Remove(previousStartBracket, (nextEndBracket - (previousStartBracket - 2)));
parsedHtmlStringBuilder.Insert(previousStartBracket, keyphraseLinkReplacement);
index = previousStartBracket + keyphraseLinkReplacement.Length;
}
}
} while (index != -1);
}
}
return parsedHtmlStringBuilder.ToString();
}
}
It's probably easiest to step through the above code, but fundamentally the parser has to:
a) find and wrap all keyphrases, ignoring case, with a link to an internal CMS, or external web resource.
b) handle an already parsed html string to both leave links in place and not create nested links.
c) allow CMS keyphrase changes to be updated in the parsed html string.
The blog of this, as well as the github code can be found from the links in the previous post.
Ok, so after finding some excellent helper posts and digging around I came up with the solution, which I've written about here:
http://frazzledcircuits.blogspot.co.uk/2015/03/umbraco-7-automatic-keyphrase.html
And the source code is here:
https://github.com/AdTarling/UmbracoSandbox
Related
In my project, i need to create an reusable "food select list" component, this component will use as food index page and food select modal window.
I created a FoodsViewComponent which inherit from AbpViewComponent and also marked as 'Widget', below are the code of this component:
[Widget(
StyleFiles = new[] { "/Pages/Shared/Components/FoodList/Default.css" },
ScriptTypes = new[] { typeof(FoodListWidgetScriptBundleContributor) },
RefreshUrl = "Widget/LoadFoodListComponent"
)]
[ViewComponent(Name = "FoodList")]
public class FoodsViewComponent : AbpViewComponent
{
private readonly IFoodAppService _foodAppService;
public FoodsViewComponent(IFoodAppService foodAppService)
{
_foodAppService = foodAppService;
}
public async Task<IViewComponentResult> InvokeAsync(
string keyword,
FoodSelectMode mode = FoodSelectMode.List,
string onclick = null,
string foodSelect = null,
string processSelect = null,
int page = 1,
int pageSize = 2)
{
PagedKeywordSearchRequestInputDto input = new()
{
Keyword = keyword,
MaxResultCount = pageSize,
SkipCount = (page - 1) * pageSize
};
PagedResultDto<FoodBaseDto> results = await _foodAppService.GetPagedListAsync(input);
ViewBag.Keyword = keyword;
ViewBag.Page = page;
ViewBag.PageSize = pageSize;
ViewBag.FoodSelectMode = mode;
ViewBag.FoodSelect = foodSelect;
ViewBag.Onclick = onclick;
ViewBag.ProcessSelect = processSelect;
return View(results);
}
}
And the FoodListWidgetScriptBundleContributor code as below:
public class FoodListWidgetScriptBundleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files
.AddIfNotContains(new string[]
{
"/Pages/Shared/Components/FoodList/Default.js" ,
"/libs/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.js"
});
}
}
The razor page of this component is as below:
#using Chriswu00.HealthEngine.FoodNutrition.Web;
#model PagedResultDto<FoodBaseDto>
#{
int p = ViewBag.Page;
int pageSize = ViewBag.PageSize;
string keyword = ViewBag.Keyword;
FoodSelectMode mode = ViewBag.FoodSelectMode;
string foodSelect = ViewBag.FoodSelect;
string componentId = $"foodsWidget_{mode}";
string onclick = ViewBag.Onclick;
string processSelect = ViewBag.ProcessSelect;
}
<div id="#componentId">
#foreach (var food in Model.Items)
{
<food-card food-id="#food.Id"
food-name="#food.Name"
food-click="#onclick"
food-select-mode="#mode"
food-select="#foodSelect"></food-card>
}
<boot-paginator link-url="/Widget/LoadFoodListComponent?keyword=#keyword&mode=#mode&foodSelect=#foodSelect&processSelect=#processSelect"
page="#p"
page-size="#pageSize"
total-items="#Model.TotalCount">
<ajax-options update-target-id="#componentId"
on-begin=""
on-complete="">
</ajax-options>
</boot-paginator>
#if (mode == FoodSelectMode.MultipleSelect ||
mode == FoodSelectMode.SingleSelect)
{
<abp-button button-type="Primary" onclick="#processSelect">Process Select</abp-button>
}
</div>
'food-card' is the custom html tag helper i created for display food item and the 'boot-paginator' is the custom html tag helper i created for pagination which has the ability to load and refresh 'the next page' by ajax.
Those component works fine, until i made a 'Modal Page' as 'Food selector'.
The Scenario is: My project is a Food and Nutrition database, user can create food and the system provids the api for getting the nutrition info of those foods. I defined two different types of food, one is just called 'Food' and the other is called 'Composite Food'. As the name suggests,the 'Composite Food' is the combination of more than one food, so during the food creation process, user can select multiple existing foods as the 'food facts' for the new food.
So i want to use a bootstrap modal window with a 'paginated food list' as the food fact selection UI, which i can integrate to the composite food creation and update pages.
I follow the 'Modals' chapter from the abp official docs (https://docs.abp.io/en/abp/6.0/UI/AspNetCore/Tag-Helpers/Modals) and create a 'FoodListModal' razor page as below:
#page
#using Microsoft.AspNetCore.Mvc.Localization
#using Chriswu00.HealthEngine.FoodNutrition.Localization;
#using Chriswu00.HealthEngine.FoodNutrition.Web.Pages.Foods;
#model Chriswu00.HealthEngine.FoodNutrition.Web.Pages.Foods.FoodListModalModel
#inject IHtmlLocalizer<FoodNutritionResource> Localizer
#{
Layout = null;
}
<abp-modal>
<abp-modal-header title="#Localizer[$"Food{Model.FoodSelectMode}"].Value"></abp-modal-header>
<abp-modal-body>
#await Component.InvokeAsync("FoodList", new
{
mode = Model.FoodSelectMode,
onclick = Model.Onclick,
foodSelect = Model.FoodSelect,
pageSize = 10,
processSelect = Model.ProcessSelect
})
</abp-modal-body>
</abp-modal>
After i integrate this modal to the composite food creation page, everything works fine except the pagination, instead of refresh within the food list, it reload the whole page. I guessed it is the problem of loading the jquery.unobtrusive-ajax.js. Because the 'Food List component' contains two js files, Default.js and the jquery.unobtrusive-ajax.js, To confirm that my guess was correct, i added an alert command into Default.js. After i tested, the Food Index page which also integrated the Food List component works fine and shows the alert when click on the pagination. But the 'Food Selector Modal' didn't shows the alert and reload the whole page.
That's what I'm currently investigating. Is this a bug of the framework? Or am I missing some knowledge point?
I have a dashboard app, that displays certain applications to the user, loaded from the database..
The tiles are draggable (Jquery).. But im struggling to understand how to move a tile, and then keep the state, so that a user can come back the next day, and still have his preferences.
I have tried to follow Telerik demo, but unfortunately, i am a student, i can't afford the license.. Has anybody done something similar, to keep a users preferences?
Here is the Telerik demo, im struggling with what would be in the models.
Local storage for state
using Microsoft.JSInterop;
using System.Text.Json;
using System.Threading.Tasks;
namespace TelerikBlazorDemos.Services
{
public class LocalStorage
{
protected IJSRuntime JSRuntimeInstance { get; set; }
public LocalStorage(IJSRuntime jsRuntime)
{
JSRuntimeInstance = jsRuntime;
}
public ValueTask SetItem(string key, object data)
{
return JSRuntimeInstance.InvokeVoidAsync(
"localStorage.setItem",
new object[] {
key,
JsonSerializer.Serialize(data)
});
}
public async Task<T> GetItem<T>(string key)
{
var data = await JSRuntimeInstance.InvokeAsync<string>("localStorage.getItem", key);
if (!string.IsNullOrEmpty(data))
{
return JsonSerializer.Deserialize<T>(data);
}
return default;
}
public ValueTask RemoveItem(string key)
{
return JSRuntimeInstance.InvokeVoidAsync("localStorage.removeItem", key);
}
}
}
This is the part im struggling with.. #Code{ }
#page "/tilelayout/persist-state"
#inject LocalStorage LocalStorage
#inject IJSRuntime JsInterop
#using TelerikBlazorDemos.Shared.DemoConfigurator
<DemoConfigurator>
<DemoConfiguratorColumn>
<TelerikButton OnClick="#SaveState" Icon="save" Class="mr-xs">Save State</TelerikButton>
<TelerikButton OnClick="#ReloadPage" Icon="reload" Class="mr-xs">Reload the page</TelerikButton>
<TelerikButton OnClick="#LoadState" Icon="download" Class="mr-xs">Load last State</TelerikButton>
<TelerikButton OnClick="#SetExplicitState" Icon="gear" Class="mr-xs">Configure State</TelerikButton>
</DemoConfiguratorColumn>
</DemoConfigurator>
<div class="demo-alert demo-alert-info" role="alert">
<strong>Change the Tile Layout</strong> (resize and reorder some tiles, remember their order) and
<strong>Save</strong> the TileLayout state, then <strong>Reload</strong> the page to see the state persisted.
<strong>Change</strong> the layout some more and <strong>Load</strong> the state to see the last one preserved.
You can manage the TileLayout state with your own code to put it in a specific configuration through the <strong>Configure</strong> button.
This demo will remember your last layout until your clear your browser storage or click the Configure button to put it in its initial state.
</div>
<div style="display: inline-block;">
<TelerikTileLayout #ref="#TileLayoutInstance"
Columns="3"
ColumnWidth="285px"
RowHeight="285px"
Resizable="true"
Reorderable="true">
<TileLayoutItems>
<TileLayoutItem HeaderText="San Francisco">
<Content>
<img class="k-card-image" draggable="false" src="images/cards/sanfran.jpg" />
</Content>
</TileLayoutItem>
<TileLayoutItem HeaderText="South Africa">
<Content>
<img class="k-card-image" draggable="false" src="images/cards/south-africa.jpg" />
</Content>
</TileLayoutItem>
<TileLayoutItem HeaderText="Sofia" RowSpan="2">
<Content>
<img class="k-card-image" draggable="false" src="images/cards/sofia.jpg" />
<div class="text-center">
<div class="k-card-body">
<p>Sofia is the largest city and capital of Bulgaria. Sofia City Province has an area of 1344 km<sup>2</sup> while the surrounding and much bigger Sofia Province is 7,059 km<sup>2</sup>. The city is situated in the western part of the country at the northern foot of the Vitosha mountain, in the Sofia Valley that is surrounded by the Balkan mountains to the north. The valley has an average altitude of 550 metres (1,800 ft). Unlike most European capitals, Sofia does not straddle any large river, but is surrounded by comparatively high mountains on all sides.</p>
<br />
<em>* The data used in this demo is taken from <em>wikipedia.com</em></em>
</div>
</div>
</Content>
</TileLayoutItem>
<TileLayoutItem HeaderText="Rome" ColSpan="2" RowSpan="2">
<Content>
<img class="k-card-image image-center" draggable="false" src="images/cards/rome.jpg" />
<div class="text-center">
<div class="k-card-body">
<p>
</p>
<p>
<em>* The data used in this demo is taken from <em>wikipedia.com</em></em>
</p>
</div>
</div>
</Content>
</TileLayoutItem>
<TileLayoutItem HeaderText="Barcelona">
<Content>
<img class="k-card-image" draggable="false" src="images/cards/barcelona.jpg" />
</Content>
</TileLayoutItem>
</TileLayoutItems>
</TelerikTileLayout>
</div>
#code { TelerikTileLayout TileLayoutInstance { get; set; }
TileLayoutState SavedState { get; set; }
string stateStorageKey = "TelerikBlazorTileLayoutStateDemoKey";
async Task SaveState()
{
var state = TileLayoutInstance.GetState();
await LocalStorage.SetItem(stateStorageKey, state);
}
async Task LoadState()
{
TileLayoutState storedState = await LocalStorage.GetItem<TileLayoutState>(stateStorageKey);
if (storedState != null)
{
TileLayoutInstance.SetState(storedState);
}
}
void ReloadPage()
{
JsInterop.InvokeVoidAsync("window.location.reload");
}
async void SetExplicitState()
{
TileLayoutState desiredState = GetDefaultDemoState();
TileLayoutInstance.SetState(desiredState);
await SaveState();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
var state = await LocalStorage.GetItem<TileLayoutState>(stateStorageKey);
if (state != null && TileLayoutInstance != null)
{
TileLayoutInstance.SetState(state);
}
}
TileLayoutState GetDefaultDemoState()
{
TileLayoutState defaultDemoState = new TileLayoutState()
{
ItemStates = new List<TileLayoutItemState>()
{
new TileLayoutItemState { Order = 1, ColSpan = 1, RowSpan = 1 },
new TileLayoutItemState { Order = 2, ColSpan = 1, RowSpan = 1 },
new TileLayoutItemState { Order = 3, ColSpan = 1, RowSpan = 2 },
new TileLayoutItemState { Order = 4, ColSpan = 2, RowSpan = 2 },
new TileLayoutItemState { Order = 5, ColSpan = 1, RowSpan = 1 },
}
};
return defaultDemoState;
} }
<style>
.k-card-image {
width: 285px;
height: 189px;
}
.k-card-body {
overflow: auto;
}
.image-center {
display: block;
margin: auto;
}
</style>
For instance TileLayoutInstance.SetState(storedState); I don't know how this is set up behind the bonnet.
p.s im using .netCore5 and visual studio 2019. Any help is much appreciated.
I am confused by your question. Are you using Telerik or are you trying to determine how their example works? If you are coding this from scratch then consider the dashboard as a set of rows and columns or a 2 dimensional array. When the user moves tiles they are switching the location of the tiles within the array. You need to store and maintain the state of the array. A solution can become more complicated depending on if the dashboard is a variable or fixed size. You can store an X and Y coordinate in the model and the tile at the location along with the overall size of the dashboard.
I have to assume that TileLayoutInstance.SetState expects an instance of TileLayoutState which contains a List of TileLayoutItemState objects. By the look of your example each TileLayoutItemState has an Order property, which determines the order of the object in the TelerikTileLayout component?
When the user moves a tile, you need to save its new position, which presumably your already doing somewhere?
You then save the current order to localStorage and read it back in when the user clicks the Load button.
From the look of it, (and apologies if I misunderstand what your trying to do or I misunderstand how this telerik component works), you then need to re-render the component using the order stored in localStorage. But your rendering code is hardcoded? It adds each TileLayoutItem in a fixed order. I see there is a ReOrderable property which presumably allows this order to be changed, but when the page reloads, I'm pretty sure that it will always render it back in the order you have provided in the HTML.
I think you will need to store the header text, P content and the image name into a list, as well as the order and then use a #ForEach loop around this List (using the correct order which you can control by using LINQ against the order property) to add each TileLayoutItem dynamically, providing the values for the src, headerText properties etc from the iterated object and render it in the persisted order.
Might be easier and more manageable to use a class and save it to a database to be honest.
Also, you can use Blazored.LocalStorage.ILocalStorageService to read/write local storage without having to write extra JS.
This is within Sitefinity if that matters, and I am really new at ASP.NET and C#.
I have an image-based navigation element at the bottom of a page that links to different articles using the same template. There are 5 articles, and I would like the link to the active page/article to be hidden so there is a grid of 4 image links.
Here's a screenshot:
https://i.imgur.com/PG2Sfpo.png
Here is the code behind it:
#{
string navTitle = string.Empty;
string url = string.Empty;
if (Model.CurrentSiteMapNode != null && Model.CurrentSiteMapNode.ParentNode != null)
{
if (Model.CurrentSiteMapNode.Title == "Home")
{
navTitle = Model.CurrentSiteMapNode.ParentNode.Title;
}
else
{
navTitle = Model.CurrentSiteMapNode.Title;
}
url = Model.CurrentSiteMapNode.ParentNode.Url;
}
}
<div class="foundation-stories-container">
#foreach (var node in Model.Nodes)
{
#RenderRootLevelNode(node);
}
</div>
#*Here is specified the rendering for the root level*#
#helper RenderRootLevelNode(NodeViewModel node)
{
string[] thisPage = (node.Url).Split('/');
string thisImage = thisPage[4] + ".jpg";
<a href="#node.Url" target="#node.LinkTarget">
<div class="foundation-story-block">
<div class="hovereffect">
<img src="[OUR WEBSITE URL]/stories/#thisImage" class="img-fluid">
<div class="overlay">
<h2>#node.Title</h2>
</div>
</div>
</div>
</a>
}
So we're already getting the page URL and image file name
string[] thisPage = (node.Url).Split('/');
string thisImage = thisPage[4] + ".jpg";
Is this as easy as doing the following?
if (thisImage = thisPage)
{
foundation-story-block.AddClassToHtmlControl("hide")
}
Seems easy enough, but I don't know where to start.
I'm better at Javascript, so I do have a JS solution in place for this already, but I'd really like to find a cleaner way to do it.
<script type="text/javascript">
$(document).ready(function() {
var active = window.location.pathname.split("/").pop()
var name = active;
name = name.replace(/-/g, ' ');
jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function(arg) {
return function( elem ) {
return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >=
0;
};
});
$("h2:Contains('" + name + "')").closest(".foundation-story-block").addClass("hide");
});
</script>
This exists on the main template page.
Gets the last part of the URL
Sets that as a variable called "name"
Changes the dash to a space if there is one (most of the pages are associated with names so it's like /first-last)
Then it goes and looks at the which is where the title of the page lives, and if it equals the "name" variable, the ".hide" class is added to the block.
Thanks for any help anyone can provide.
You could bind a click event to your elements with the foundation-story-block class. The reason I use .on instead of .click is because when using UpdatePanels the click event won't fire after an UpdatePanel has it's update event triggered - you might encounter a similar problem with your dynamic binding so I used .on to avoid this.
$(".foundation-story-block").on("click", function() {
// Remove the "hide" class from any elements that have it applied
$.each($(".foundation-story-block.hide"), function(index, value) {
// Remove the class using the "this" context from the anonymous function
$(this).removeClass("hide");
});
// Add the "hide" class to the element that was clicked
$(this).addClass("hide");
});
I haven't run this though an IDE so it might not be 100% correct but it will put you on the correct path.
It is possible, yes. Here is how:
...
#{
var hiddenClass = thisImage == thisPage ? "hide" : string.Empty;
}
<div class="foundation-story-block #hiddenClass">
<div class="hovereffect">
<img src="[OUR WEBSITE URL]/stories/#thisImage" class="img-fluid">
<div class="overlay">
<h2>#node.Title</h2>
</div>
</div>
</div>
I am using itextsharp library.I design an HTML page and convert to PDF .in that case some table are not split perfectly and row also not split correctly
table.keepTogether;
table.splitRows;
table.splitLate
I try this extension but it does not work correctly it mess my CSS and data in PDF. if you have method..answer me:)
finally i got it
public class TableProcessor : Table
{
const string NO_ROW_SPLIT = "no-row-split";
public override IList<IElement> End(IWorkerContext ctx, Tag tag, IList<IElement> currentContent)
{
IList<IElement> result = base.End(ctx, tag, currentContent);
var table = (PdfPTable)result[0];
if (tag.Attributes.ContainsKey(NO_ROW_SPLIT))
{
// if not set, table **may** be forwarded to next page
table.KeepTogether = false;
// next two properties keep <tr> together if possible
table.SplitRows = true;
table.SplitLate = true;
}
return new List<IElement>() { table };
}
}
use this class and
var tagfac = Tags.GetHtmlTagProcessorFactory();
tagfac.AddProcessor(new TableProcessor(), new string[] { HTML.Tag.TABLE });
htmlContext.SetTagFactory(tagfac);
integrate this method with htmlpipeline context
this method every time run when the tag hasno-row-split.if it contain key it will make kept by keep together keyword to make page breaking
html
<table no-row-split style="width:100%">
<tr>
<td>
</td>
</tr>
</table>
This is long-winded but should be easy for one of you knowledgable chaps to workout.
I have a DotNetNuke webpage with a dynamic login link. If you are not logged in the link will be 'login' and have the appropriate URL to a login popup. If you are logged in the link will be 'logout' and likewise have an the appropriate URL to the webpage that handles logout.
When the page determines if you are logged in or not the HTML link gets built with an attribute of : onclick="return dnnModal.show('http://blahblah.com....').
The code that does this:
loginLink.Attributes.Add(" onclick", "return " + UrlUtils.PopUpUrl(loginLink.NavigateUrl, this, PortalSettings, true, false, 200, 550));
Regardless of what the link is, the ID and Class always remain the same. My problem is that I would like to replace the login text with an image, infact a different image for login and logout. The issue here is that because the ID and Class stay the same I can't just do it via CSS as I normally would, but I have been able to style classes based on their attributes. I have tested this by finding out the output of the creation of the HTML link and styling the class based on the 'href' attribute for example:
a #dnn_dnnLogin_loginLink .LoginLink [href="http://some very very long dynamically created URL.aspx"]{ styles here }
The problem with this is the login/logout links change based on what page you are currently on.
I do know that each of the two rendered options has a uniqe attribue that I could style and that's their "Text" attribute. So quite simply how do I add this attribute to be rendered in HTML so that I can style it with CSS?
I have tried several variations such as:
loginLink.Attributes.Add(" onclick", "return " + UrlUtils.PopUpUrl(loginLink.NavigateUrl, this, PortalSettings, true, false, 200, 550) " Text", + loginLink.Text);
In the hope that what would be rendered would be something like:
onclick="return dnnModal.show('http://localhost/CPD/tabid/87/ctl/Login/Default.aspx?returnurl=%2fCPD.aspx&popUp=true',/*showReturn*/true,200,550,true,'')" Text="Login"
So I could style:
a #dnn_dnnLogin_loginLink .LoginLink [Text="Login"]{styles here}
a #dnn_dnnLogin_loginLink .LoginLink [Text="Logout"]{styles here}
But instead I get a generic error. I have tried various ways of writing the line without success, I just don't know the syntax.
Could someone point me in the right direction? I so hope I'm not barking up the wrong tree as this would be a really simple solution to my initial problem.
Thanks,
Edit - Code for the whole page if that helps?
using System;
using System.Web;
using System.Web.UI;
using DotNetNuke.Common;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Services.Exceptions;
using DotNetNuke.Services.Localization;
using DotNetNuke.UI.Modules;
namespace DotNetNuke.UI.Skins.Controls
{
public partial class Login : SkinObjectBase
{
private const string MyFileName = "Login.ascx";
public string Text { get; set; }
public string CssClass { get; set; }
public string LogoffText { get; set; }
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
try
{
if (!String.IsNullOrEmpty(CssClass))
{
loginLink.CssClass = CssClass;
}
if (Request.IsAuthenticated)
{
if (!String.IsNullOrEmpty(LogoffText))
{
if (LogoffText.IndexOf("src=") != -1)
{
LogoffText = LogoffText.Replace("src=\"", "src=\"" + PortalSettings.ActiveTab.SkinPath);
}
loginLink.Text = LogoffText;
}
else
{
loginLink.Text = Localization.GetString("Logout", Localization.GetResourceFile(this, MyFileName));
}
loginLink.NavigateUrl = Globals.NavigateURL(PortalSettings.ActiveTab.TabID, "Logoff");
}
else
{
if (!String.IsNullOrEmpty(Text))
{
if (Text.IndexOf("src=") != -1)
{
Text = Text.Replace("src=\"", "src=\"" + PortalSettings.ActiveTab.SkinPath);
}
loginLink.Text = Text;
}
else
{
loginLink.Text = Localization.GetString("Login", Localization.GetResourceFile(this, MyFileName));
}
string returnUrl = HttpContext.Current.Request.RawUrl;
if (returnUrl.IndexOf("?returnurl=") != -1)
{
returnUrl = returnUrl.Substring(0, returnUrl.IndexOf("?returnurl="));
}
returnUrl = HttpUtility.UrlEncode(returnUrl);
loginLink.NavigateUrl = Globals.LoginURL(returnUrl, (Request.QueryString["override"] != null));
if (PortalSettings.EnablePopUps && PortalSettings.LoginTabId == Null.NullInteger)
{
loginLink.Attributes.Add(" onclick", "return " + UrlUtils.PopUpUrl(loginLink.NavigateUrl, this, PortalSettings, true, false, 200, 550));
}
}
}
catch (Exception exc)
{
Exceptions.ProcessModuleLoadException(this, exc);
}
}
}
}
CSS classes are just designed for this purpose, and they are supported by all browsers that use CSS styling (even very old ones). You don't have to fight around with obscure selectors that are referencing some link that could change and break your styling again.
Since you said you already have a class assigned to these tags, you just want to specify an additional one. You can have more than one class assinged to a tag. See the W3C css class page for more info, in section 'Attribute Values':
Specifies one or more class names for an element. To specify multiple
classes, separate the class names with a space, e.g. . This allows you to combine several CSS classes for one
HTML element.
You can set the second class simply by appending it to the WebControl.CssClass string, separated by a space:
loginLink.CssClass = loginLink.CssClass + " login";
or
loginLink.CssClass = loginLink.CssClass + " logout";
this way you can access it via a single class selector or even the multiple class selector (only selects those tags that have both classes assigned) in your CSS style sheet:
.LoginLink.login { /* styles here */ }
.LoginLink.logout { /* styles here */ }
The text on the login/logout button is not stored in the Text="" attribute, but in the InnerHTML node. So your CSS selector would not apply. (I also think that the spacings in the selector are wrong, and that this solution would not support multilingual buttons, etc.)
Usually this type of styling would be implemented by in the Skin Editor (Admin/Skins/scroll down to section Skin Designer), where you select Skin or Container, File, Token=LOGIN, Setting=Text and LogoffText, and add a value src=path/to/a.gif. However, the skin designer seems to be broken in 6.1.x (bug report)
You might still try and have a look at the login.ascx and login.ascx.cs files in the admin\Skins directory of your DNN installation. Edit the code to assign loginLink.ImageUrl depending on Request.IsAuthenticated.