Create read more link in AngularJS - angularjs

I want to create a link with read more text. if there are more than 3 lines in a paragraph this link should be visible and clicking on this it show display all the lines.

I wanted to do the same thing, so I created a directive. Have a look here: https://gist.github.com/doukasd/0744566c5494ebc8643f
Usage is pretty simple:
<p data-dd-collapse-text="100">{{veryLongText}}</p>
Where 100 is the character limit you want to specify.
UPDATE: dd-text-collapse

For read more , you can use angular limitTo filter for limiting your paragraph to character's length instead of limiting paragraph to line numbers .
You can use something like this :
in html
<p> {{myString | limitTo:numLimit}} </p>
<button type='button' ng-click="readMore()">read more</button>
in controller
$scope.numLimit=200;
$scope.readMore=function(){
$scope.numLimit=10000;
};

Based on some of the information here I've put together a nice show more/less implementation
Directive Definition
showMore.js
.directive('showMore',
function(){
return {
templateUrl: 'showMore.html',
restrict: 'A',
transclude: true,
scope:{
'showMoreHeight': '#'
},
controller: ['$scope', '$element', '$interval', function($scope, $element, $interval) {
$scope.expanded = false;
$interval(function(){
renderStyles();
}, 300);
$scope.expandable = false;
function renderStyles(){
if($element.height() >= $scope.showMoreHeight && $scope.expanded === false){
$scope.expandable = true;
}
}
$scope.showLessStyle = {
'max-height': $scope.showMoreHeight + 'px',
'overflow': 'hidden'
};
}]
};
});
showMore.html
<span>
<div ng-style='showLessStyle' ng-hide='expanded' ng-transclude>
</div>
<div ng-show='expanded' ng-transclude>
</div>
<a style='float:right;' ng-hide='expanded || !expandable' ng-click='expanded = !expanded' localize>show more</a>
<a style='float:right;' ng-show='expanded && expandable' ng-click='expanded = !expanded'>show less</a>
</span>
The usage is fairly simple just transclude what ever you want to show more/less of
USEAGE:
<div show-more show-more-height='150'>
<div class='showMoreContent'></div>
</div>
I hope this is somewhat helpful!

There can be some workaround.
basic idea is like this
1. at first, use javascript to calculate the original height
2. if higher than 3 lines , set the overflow to hidden and show a button. The button is used to toggle state
3. if not, do nothing
About the button.
If the button is out of text area, it's no problem for you I think.
If you need the button inline within the element, there are 2 possibilitis.
if you want it position fixed at right-bottom,
create a gradient background for the button, fade-out effect.
It looks nice and very simple.
here is jsfiddle: http://jsfiddle.net/sunderls/HYHZ6/ .
Everytime toggling the state, you actually change the class of the element.
if you want it right after the end of text? it's a little tricky, since even if text height is 3 lines, you cannot be sure
whether it remains 3 lines height , after appending a button next to
it.
I think one way is to use Range API, to calculate the paragraph
dimensions, (I've used the api in my collamark.com, it's powerful
but different browsers have different behaviors). Actually you can
get the dimensions of every lines dynamically. And by some
backwards loops, you can get the appropriate substring of the text,
to show in collapsed mode, which is just right for 3 lines height
with a button. (the api doc is here:
https://developer.mozilla.org/en-US/docs/Web/API/range)
So now every time you toggle the state, you actually is change the
text. not the class.
Here begins the Code when do it in Angular
template
first, this kind of feature is a standalone module which you can reuse everywhere, so in your template, we create a new directive autoFolded, sth like this:
<auto-folded>
<p>Some text here, maybe long maybe short</p>
</auto-folded>
directive.coffee
then we handle all the logic in directive definition( sorry for writing in coffee though)
directive('autoFolded',[
'$window'
($window) ->
return {
restrict: 'E'
transclude: true
template: '<div class="auto-folded"><div ng-transclude></div><a href ng-click="toggleFoldedState()" class="auto-folded--more"></a></div>'
link: (scope, element, attrs)->
$$window = $ $window
content = $(element).find('.auto-folded')
toggleFoldedState = ->
if content.hasClass 'auto-folded--folded'
content.removeClass('auto-folded--folded').addClass('auto-folded--unfolded')
else if content.hasClass 'auto-folded--unfolded'
content.removeClass('auto-folded--unfolded').addClass('auto-folded--folded')
return
scope.toggleFoldedState = toggleFoldedState
init = ()->
contentHeight = content.outerHeight()
if contentHeight > 48
content.addClass 'auto-folded--folded'
content.show()
$$window.on 'ngcontentloaded',init
}
])
here is the explaination
for this directive contains text it doesn't know, so it's a translucent direcitve. Like a modal popup, it contains the Text and a toggle button.
restrict: 'E'
transclude: true
template: '<div class="auto-folded"><div ng-transclude></div><a href ng-click="toggleFoldedState()" class="auto-folded--more"></a></div>'
when clicking the button, it actually do the toggling. If unfolded, then fold it; If foled, then unfolded. We accomplish this by toggling the classNames, the cold is straightforward
toggleFoldedState = ->
content.css 'color','red'
if content.hasClass 'auto-folded--folded'
content.removeClass('auto-folded--folded').addClass('auto-folded--unfolded')
else if content.hasClass 'auto-folded--unfolded'
content.removeClass('auto-folded--unfolded').addClass('auto-folded--folded')
return
and we use ng-click="toggleFoldedState()" to bind this action to the toggling button
We have to do some initial work to fold the text if it's tall enough at page loaded. However, link function of directive is to create the actual Dom, before dom rendering.So in link, we cannot know the height, that's why we register the init() to ngcontentloaded event:
init = ()->
contentHeight = content.outerHeight()
if contentHeight > 48
content.addClass 'auto-folded--folded'
content.show()
$$window.on 'ngcontentloaded',init
here I use 48px as 3-line-height, you can define your own, or calculate dynamically from the dom, like content.css('lineHeight').
since this is done after dom rendering, so the text is already displayed before init(). There will be a ugly slideUp effect. That's why we first hide the dom using css(as following), and content.show() in init
So we are done with directive, the folded/unfoled state are controlled by className. here we go.
css.sass
(sorry I wrote it in sass)
.auto-folded
display: none //only display after init()
position: relative
.auto-folded--more //the button is placed at right-bottom, and default to hidden
display: none
position: absolute
right: 0
bottom: 0
&.auto-folded--folded //when folded, set maxHeight, and overflow to hidden
max-height: 48px
overflow: hidden
.auto-folded--more // toggling button is displayed,
display: block
&:before // and it's text is "more"
content: "more"
&.auto-folded--unfolded //when unfoled, s
.auto-folded--more // toggling button is displayed
display: block
&:before // and it's text is "hide"
content: "hide"
So the text of toggling button and visibility of it , are all controlled by the class of its parent.
for the parent
1. if text is not 3-line height, it will only have 'auto-folded' as css class, so the button is hidden
if text is over 3-line height, in init() process, it's classNames will be 'auto-folded auto-folded--folded'.
then the button is shown. clicking it will toggle parent's classNames between 'auto-folded--folded' and 'auto-folded--unfolded'

Related

how to toggle medium editor option on click using angularjs

I am trying to toggle the medium editor option (disableEditing) on button click. On the click the value for the medium editor option is changed but the medium editor does not use 'updated' value.
AngularJS Controller
angular.module('myApp').controller('MyCtrl',
function MyCtrl($scope) {
$scope.isDisableEdit = false;
});
Html Template
<div ng-app='myApp' ng-controller="MyCtrl">
<span class='position-left' medium-editor ng-model='editModel' bind-options="{'disableEditing': isDisableEdit, 'placeholder': {'text': 'type here'}}"></span>
<button class='position-right' ng-click='isDisableEdit = !isDisableEdit'>
Click to Toggle Editing
</button>
<span class='position-right'>
toggle value - {{isDisableEdit}}
</span>
</div>
I have created a jsfiddle demo.
I think initialising medium editor on 'click' could solve the issue, but i am not sure how to do that either.
using thijsw angular medium editor and yabwe medium editor
For this specific use case, you could try just disabling/enabling the editor when the button is clicked:
var editor = new MediumEditor(iElement);
function onClick(event) {
if (editor.isActive) {
editor.destroy();
} else {
editor.setup();
}
}
In the above example, the onClick function is a handler for that toggle button you defined.
If you're just trying to enable/disable the user's ability to edit, I think those helpers should work for you.
MediumEditor does not currently support changing configuration options on an already existing instance. So, if you were actually trying to change a value for a MediumEditor option (ie disableEditing) you would need to .destroy() the previous instance, and create a new instance of the editor:
var editor = new MediumEditor(iElement),
editingAllowed = true;
function onClick(event) {
editor.destroy();
if (editingAllowed) {
editor = new MediumEditor(iElement, { disableEditing: true });
} else {
editor = new MediumEditor(iElement);
}
editingAllowed = !editingAllowed;
}
Once instantiated, you can use .setup() and .destroy() helper methods to tear-down and re-initialize the editor respectively. However, you cannot pass new options unless you create a new instance of the editor itself.
One last note, you were calling the init() method above. This method is not officially supported or documented and it may be going away in future releases, so I would definitely avoid calling that method if you can.
Or you could just use this dirty hack : duplicate the medium-editor element (one with disableEditing enabled, the other with disableEditing disabled), and show only one at a time with ng-show / ng-hide :)
<span ng-show='isDisableEdit' class='position-left' medium-editor ng-model='editModel' bind-options="{'disableEditing': true ,'disableReturn': isDisableEdit, 'placeholder': {'text': 'type here'}}"></span>
<span ng-hide='isDisableEdit' class='position-left' medium-editor ng-model='editModel' bind-options="{'disableEditing':false ,'disableReturn': isDisableEdit, 'placeholder': {'text': 'type here'}}"></span>
You can see jsfiddle.

Strange behaviour of angular bootstrap collapse

I faced with strange behaviour of uib-collapse.
Let's assume I have a list of elements and i want each of them to be collapsed. Also i want to refresh its content periodically depend on something.
For example: i have some items and each of them have description which consists of some sections. I can pick item and description sections should be populated with item's description content. The problem is that each time i refresh its content, some sections are collapsing (despite the fact i set uib-collapse to false)
My controller:
var i = 0;
$scope.sections = [0,1,2];
$scope.next = function(nextOffset) {
i+=nextOffset;
$scope.sections = [i, i+1, i+2]
}
My template:
<button ng-click="next(1)" style="margin-bottom: 10px">Next item</button>
<button ng-click="next(2)" style="margin-bottom: 10px">Next next item</button>
<button ng-click="next(3)" style="margin-bottom: 10px">Next next next item</button>
<div ng-repeat="section in sections">
<div uib-collapse="false">
<div class="well well-lg">{{ section }}</div>
</div>
</div>
So when i click first button, only one section does transition. When i click second, 2 section do transition and click to third button leads to all section transition.
See plunkr
Any ideas?
UPD: if $scope.sections is array of object, not of primitives, then all sections have transition in each of 3 cases. It is so ugly...
You are not refreshing the existing content, you are adding new arrays each time, which will make ng-repeat remove the old DOM elements and insert new ones.
If you try with track by $index you will see the difference:
<div ng-repeat="section in primitiveSections track by $index">
Demo: http://plnkr.co/edit/hTsVBrRLa8nWXhaqfhVK?p=preview
Note that track by $index might not be the solution you want in your real application, I just used it for demonstration purposes.
What you probably need is to just modify the existing objects in the array.
For example:
$scope.nextObject = function(nextOffset) {
j += nextOffset;
$scope.objectSections.forEach(function (o, i) {
o.content = j + i;
});
};
Demo: http://plnkr.co/edit/STxy1lAUGnyxmKL7jYJH?p=preview
Update
From the collapse source code:
scope.$watch(attrs.uibCollapse, function(shouldCollapse) {
if (shouldCollapse) {
collapse();
} else {
expand();
}
});
When a new item is added the watch listener will execute, shouldCollapse will always be false in your case so it will execute the expand function.
The expand function will always perform the animation:
function expand() {
element.removeClass('collapse')
.addClass('collapsing')
.attr('aria-expanded', true)
.attr('aria-hidden', false);
if ($animateCss) {
$animateCss(element, {
addClass: 'in',
easing: 'ease',
to: {
height: element[0].scrollHeight + 'px'
}
}).start().finally(expandDone);
} else {
$animate.addClass(element, 'in', {
to: {
height: element[0].scrollHeight + 'px'
}
}).then(expandDone);
}
}
If this is the intended behavior or not I don't know, but this is the reason why it happens.
this is a comment on the original ui-bootstrap library: (and the new uib prefixed directive doesn't comply this comment.)
// IMPORTANT: The height must be set before adding "collapsing" class.
Otherwise, the browser attempts to animate from height 0 (in
collapsing class) to the given height here.
use the deprecated "collapse" directive instead of new "uib-collapse" until it gets fixed.

How to find out if something in a given div has focus?

Using angularjs, I'm showing a 2-level list like this
- first main item
- first subitem of the first main item
- second subitem of the first main item
- AN EMPTY ITEM AS PLACEHOLDER TO ENTER THE NEXT SUBITEM
- second main item
- first subitem of the second main item
- second subitem of the second main item
- AN EMPTY ITEM AS PLACEHOLDER TO ENTER THE NEXT SUBITEM
In order to save place, I'd like to show the PLACEHOLDER only if anything in the corresponding div has focus, so that there's only one such placeholder. I know that there's ngFocus, but I'd prefer something simpler than creating tons of event handlers. Maybe something like this :
<div ng-focus-model="mainItem.hasFocus" ng-repeat="mainItem in list">
... main item line
... all subitems
</div>
A unidirectional binding would be sufficient as I don't need to set the focus.
The problem here is the following; we want to avoid adding event listener to each and every child, but add it only to the parent. The parent will be responsible for taking the appropriate action. The general solution to this, is to use even propagation (delegation). We attach only one listener to the parent, when an event occurs on the child (focus on input element in this example), it will bubble up to the parent and the parent will execute the listener.
Here's the directive:
app.directive('ngFocusModel', function () {
return function (scope, element) {
var focusListener = function () {
scope.hasFocus = true;
scope.$digest();
};
var blurListener = function () {
scope.hasFocus = false;
scope.$digest();
};
element[0].addEventListener('focus', focusListener, true);
element[0].addEventListener('blur', blurListener, true);
};
});
The directive listens for events and accordingly sets the value on scope, so we can make conditional changes.
There are several things to notice here.
focus and blur events don't "bubble", we need to use "event capturing" to catch them. That's why element.on('focus/blur') is not used (it doesn't allow for capture, afaik) but an addEventListener method. This method allows us to specify if the listener will be executed on "event bubbling" or "event capturing" by setting the third argument to false or true accordingly.
We could have used focusin and focusout events which "bubble", unfortunatelly these aren't supported in Firefox (focusin and focusout).
Here's a plunker with the implementation.
Update:
It occurred to me that this can be done with pure CSS using the :focus pseudo-class, the only downside is that the placeholder needs to be in proper position (sibling) relative to the input elements. See codepen.
Unfortunately the only rock solid way to do what you want is to respond to the focus\blur events on the inputs...that's the only way to get notified.
You could put a hidden input as the first element in each div and put the NgFocus attribute on it but that only works if a user tabs into it.
DEMO
I created a small directive that can be used for what you need:
app.directive('childFocus', function($window){
var registered = [];
// observing focus events in single place
$window.addEventListener('focus', function(event){
registered.forEach(function(element){
if(element.contains(event.target)){
// if element with focus is a descendant of the
// element with our directive then action is triggered
element._scope.$apply(element._scope.action);
}
});
}, true)
return {
scope : {
action : '&childFocus' // you can pass whatever expression here
},
link : function(scope, element){
// keep track ref to scope object
element[0]._scope = scope;
// (probably better would be to register
// scope with attached element)
registered.push(element[0]);
scope.$on('destroy', function(){
registered.splice(registered.indexOf(element[0]),1);
});
}
}
});
You could use the focus event of the '.parent *' selector to capture any focus events, then loop through each of the parent DIVs and use the :focus JQuery selector to check for child elements with focus, then add a class to the parent DIV and use that class to show/hide the placholder (see this jsfiddle):
$(function(){
$('.parent *').focus(function(){
$('.selected').removeClass('selected');
$('.parent').each(function(index, el){
(function($el){
setTimeout(function(){
console.log($el.attr('id'));
if($el.find(':focus').length){
$el.addClass('selected');
}
});
})($(el));
});
});
});
.parent{
padding:1rem;
margin:1rem;
border:solid 1px green;
}
.selected{
border:solid 1px red;
}
.parent .placeholder{
display:none;
}
.parent.selected .placeholder{
display:block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class='parent' id='div1'>
<input type="text" />
<div class='placeholder'>Placeholder</div>
</div>
<div class='parent' id='div2'>
<input type="text" />
<div class='placeholder'>Placeholder</div>
</div>
<div class='parent' id='div3'>
<input type="text" />
<div class='placeholder'>Placeholder</div>
</div>
<div class='parent' id='div4'>
<input type="text" />
<div class='placeholder'>Placeholder</div>
</div>

Angular-dragdrop or jQueryUI calculating droppable area offset incorrectly

Been trying to use angular-dragdrop, but the droppable area that is created isn't the same size as the div (it is both taller and wider), and sits directly underneath where it should be. I have a feeling it's calculating the offsets incorrectly, but I'm not exactly sure how, nor how to fix it.
Here is the directive (pardon the bad formatting and possibly bad practices):
mainApp.directive("mppDroppableLogo", function () {
return {
restrict: 'E',
replace: false,
transclude: true,
templateUrl: 'src/slides/editing/mppDroppableLogo.tpl.html',
controller: 'mppDroppableLogoController',
scope: {
imageUrlChanged: '&'
},
compile: function compile(element, attributes) {
return {
pre: function preLink(scope, element, attributes, controller) { },
post: function postLink(scope, element, attributes, controller) {
var sitecoreImage = element.find('img');
var imageWrapper = element.find(".mpp-droppable-logo");
if (!angular.isDefined(imageWrapper)) {
console.log("mppDroppable:pgp directive is missing.");
}
if (angular.isDefined(sitecoreImage)) {
var styling = sitecoreImage.attr('style');
if (angular.isDefined(styling)) {
imageWrapper.attr("style", styling);
}
$scope.originalImageUrl = sitecoreImage.attr("src");
var cssUrl = "url(\'" + sitecoreImage.attr("src") + "\')";
imageWrapper.css("background-image", cssUrl);
}
imageWrapper.css("background-size", "contain");
imageWrapper.css("background-repeat", "no-repeat");
}
};
}
};
})
And its partial HTML:
<div data-drop="true"
data-jqyoui-options="{
hoverClass: 'valid-item-being-dragged',
tolerance: 'touch'
}"
jqyoui-droppable="{
onDrop: 'changeUrl(event, ui)'
}"
class="mpp-droppable-logo"
ng-click="toggleSidebar()">
<!-- Hidden original image generated by Sitecore -->
<div ng-transclude ng-hide="true"></div>
</div>
This is used to wrap a sitecore image tag in an XSLT:
<div style="top: 26vw; left: 6vw;">
<mpp-droppable-logo image-url-changed="settings.imageUrlChanged" field-name="Logo">
<sc:image field="Logo" style="width: 16vw; height: 5.5vw;" />
</mpp-droppable-logo>
</div>
So what happens is that the directive gets compiled, hides the sitecore image tag, and then fills the div that wraps it (imageWrapper in the post compile, the div defined in the partial HTML with data-drop=true) with stylings to imitate an image tag. I've included an image below, with bad MS Paint skills to cover up proprietary stuff. The yellow-bordered div represents where the droppable area SHOULD be, and is only shown when a valid draggable asset enters the droppable area. The black square is the image I'm currently dragging - as you can see, we're way out of the bounds of the droppable area. The blue rectangle represents approximately the bounds of where the actual droppable area is.
http://gyazo.com/948852b8c96d3c6f3eeccec5ba2e11d9
If it's any use:
removing the "top" and "left" styles from the outer wrapper will move the image up to the top left (obviously), but the droppable area will now also somewhat overlap the div, so you can drag images directly onto it, but you can also drag images slightly below it and the proper event will fire.
removing the hidden sitecore image tag after transferring over the styles doesn't change the droppable area's size or placement
Jquery offset() on the div gives: top=449 and left=367. Jquery position() on the div gives top=0, left=0.
Anyone have any ideas as to what the problem might be?

AngularJS animate dynamic margin

I have an element that appears when the user clicks a button elsewhere on the screen. The element appears to come out of the top of the screen. The element by default needs to be tucked out of view above the screen, so I will have a margin-top style that is based on the height of the element (and will be a negative value). This cannot be hardcoded in css because the element height may vary. When I click the button, I want the element margin-top to change to 0 and I want a transition animation.
The sample shown on angularJS documentation is for adding a removing a class. This would work fine if I knew the values to be set and could code them in CSS, however I cannot. What is the correct way to solve this?
The code below works for displaying and hiding my element using a margin but there is no animation. How do I trigger an animation here when the margin changes?
https://docs.angularjs.org/guide/animations
Quote Total: {{salesPriceTotal + taxesTotal - tradeInsTotal | currency}}
<div class="totals" ng-style="setTopMargin()">
// totals stuff here.
</div>
$scope.setTopMargin = function() {
return {
marginTop: $scope.marginTop
}
};
$scope.$watch('showTotals', function() {
var margin = $scope.showTotals ? 10 : -160 + $scope.modelTotals.length * -200;
$scope.marginTop = margin.toString() + 'px';
});
I added the following code per a suggested solution, but this code is never hit.
myApp.animation('.totals', function () {
return {
move: function (element, done) {
element.css('opacity', 0);
jQuery(element).animate({
opacity: 1
}, done);
// optional onDone or onCancel callback
// function to handle any post-animation
// cleanup operations
return function (isCancelled) {
if (isCancelled) {
jQuery(element).stop();
}
}
},
}
});
As the documentation explains: "The same approach to animation can be used using JavaScript code (jQuery is used within to perform animations)".
So you basically needs to use animate() from jQuery to do what you want.

Resources