I would like to synchronize the width of DOM elements such that the width is always the smallest width that can accommodate the content. I can successfully increase the size by $watching for an increase (based on ref1 & ref2), but I can't figure out how to shrink the element after it's been enlarged.
Angular directive:
app.directive('syncWidth', function () { //declaration; identifier master
return {
restrict: 'A',
link: function (scope, element) {
var linkElement = element[0];
scope.$watch(
function () {
return (linkElement.scrollWidth);
},
function (newValue, oldValue) {
if (newValue > oldValue) {
scope.testWidth = {
'min-width': newValue + 'px'
}
}
},
true
);
}
};
});
HTML:
<input type="text" ng-model="myText">
<input type="text" ng-model="myText2">
<div sync-width="test" style="background-color: lightblue" ng-style="testWidth">1 [{{myText}}]</div>
<div sync-width="test" style="background-color: lightcoral" ng-style="testWidth">2 [{{myText2}}]</div>
I'm assuming the issue is that the two elements are referencing another. I'm thinking the pseudocode would look something like:
Set a global groupMinWidth variable
Detect element.contentWidth changed
If newContentWidth > groupMinWidth {
groupMinWidth = newContentWidth;
}
If oldContentWidth == groupMinWidth {
check all synchronized elements and set groupMinWidth to the largest contentWidth;
}
Thanks for the help.
I'm now working on the plunkr but not fully working yet : https://plnkr.co/edit/6XfGMzI1su8cpHjkyDWC?p=preview . The point is $watching the change on the text, when the text gets reduced, use a shared value/global value to record and check which one's width is the widest , set the min-width to that value to both of the elements.
===EDIT===
Finally make this work: https://plnkr.co/edit/c2FOu5LfPfGfnkU12KTG?p=preview
So the principle behind this is : I need a changeable div to measure the scrollWidth/offsetWidth when the content changed. So I add a new nested div which include the content. I made the inner div as a float and once the content changed I will update the outer div's width by setting it's width to the inner div's width. The key point is because the inner div is FLOAT, it can overflows the outer div's right border, but the out div is watching the change on the content, so the outer div's width will be updated immediately.
Related
I have a select with ngOptions based on an array. This array can change.
If the new array value does not contain the selected option value, the option value is set to undefined by the selectController. Is there a way to prevent this ?
Plunker : https://plnkr.co/edit/kao3h5ivHXlP1Wrdx1Ib?p=preview
Scenario:
select Blue/Red or Green color
click on Reduced to only have Black and White options
See that the model value is left to blank
Wanted behavior : that the model value stays at Blue/Red or Green
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="//code.angularjs.org/snapshot/angular.min.js"></script>
</head>
<body ng-app="selectExample">
<script>
angular.module('selectExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.colorsFull = [
{id:"bk", name:'black'},
{id:"w", name:'white'},
{id:"r", name:'red'},
{id:"be", name:'blue'},
{id:"y", name:'yellow'}
];
$scope.colors = $scope.colorsFull;
$scope.selectedColor =$scope.colorsFull[0];
$scope.colorsReduced = [
{id:"bk", name:'black2'},
{id:"w", name:'white2'}];
}]);
</script>
<div ng-controller="ExampleController">
<button ng-click="colors=colorsReduced">Reduced</button>
<button ng-click="colors=colorsFull">Full</button>
<br/>
Colors : {{colors}}
<hr/>
<select ng-model="selectedColor" ng-options="color.name for color in colors track by color.id">
</select>
selectedColor:{{selectedColor}}
</div>
</body>
</html>
You can achieve this by keeping track of what color is selected in the full colors dropdown, and inserting it into the reduced colors array. First, add an ng-change directive so that we can keep track of the selected color:
<select ng-model="selectedColor" ng-options="color.name for color in colors track by color.id" ng-change="setColor(selectedColor)">
And in your controller:
$scope.setColor = function(color) {
if(color !== null) {
// Keep track of the color that is selected
$scope.previousColor = color;
}
else {
// Changed arrays, keep selected color in model
$scope.selectedColor = $scope.previousColor;
}
}
Now ng-model is set to the correct color whenever the arrays are changed, but it will appear blank in the reduced colors dropdown because the option doesn't exist. So, we need to insert that option into the array. However, switching back and forth between dropdowns will cause the reduced colors array to keep on adding more options, and we only want to remember the option we selected from the full colors array. So, we need create an initial set of colors to revert back to when switching.
// Keep a copy of the original set of reduced colors
$scope.colorsReducedInitial = [
{id:"bk", name:'black2'},
{id:"w", name:'white2'}];
Finally, we need to insert the selected option into the reduced colors array. Change the ng-click on the Reduced button to use a function:
<button ng-click="setColorsReduced()">Reduced</button>
Now, we can insert the option, after resetting the reduced colors array to its initial state:
$scope.setColorsReduced = function() {
// Revert back to the initial set of reduced colors
$scope.colors = angular.copy($scope.colorsReducedInitial);
if($scope.previousColor !== undefined) {
var found = false;
angular.forEach($scope.colorsReducedInitial, function(value, key) {
if(value.id == $scope.previousColor.id) {
found = true;
}
});
// If the id is found, no need to push the previousColor
if(!found) {
$scope.colors.push($scope.previousColor);
}
}
}
Note that we are looping through the reduced colors array to ensure we aren't duplicating any colors, such as black or white.
Now, the reduced colors ng-model has the previous dropdown's selected color.
Updated Plunkr Demo
Use below code in script
$scope.makeSelected=function(){
$scope.selectedColor =$scope.colorsReduced[0];
}
And just add this function call in reduced button line like below
<button ng-click="colors=colorsReduced;makeSelected()">Reduced</button>
This will do what you want to achieve.
Using Jukebox's answer, I ended-up writing a directive, using the modelCtrl.$formatters to get the initial value. It also offer the possibility to store the previousValue in the scope or in a local variable :
Usage: <select .... select-keep> or <select .... select-keep="previousColor">
Directive:
.directive('selectKeep', function($parse) {
return {
require: 'ngModel',
link: function (scope, element, attrs, modelCtrl) {
var previousValueGetter;
var previousValueSetter;
if (attrs.selectKeep) { //use a scope attribute to store the previousValue
previousValueGetter = $parse(attrs.selectKeep);
previousValueSetter = previousValueGetter.assign;
}
else { //use a local variable to store the previousValue
var previousValue;
previousValueGetter = function(s) { return previousValue;};
previousValueSetter = function(s, v) { previousValue = v;};
}
//store the initial value
modelCtrl.$formatters.push(function(v) {
previousValueSetter(scope, v);
return v;
});
//get notified of model changes (copied from Jukebox's answer)
modelCtrl.$viewChangeListeners.push(function() {
if (modelCtrl.$modelValue !== null) {
previousValueSetter(scope, modelCtrl.$modelValue);
} else {
modelCtrl.$setViewValue(previousValueGetter(scope));
}
});
}
};
Plunker
Edit : it has a flaw, the form gets dirty even if the value does not change. I had to add these lines in the else of the viewChangeListener but it doesn't look nice. Any ideas ?:
...
} else {
modelCtrl.$setViewValue(previousValueGetter(scope));
//set pristine since this change is not a real change
modelCtrl.$setPristine(true);
//check if any other modelCtrl is dirty. If not, we will have to put the form as pristine too
var oneDirty =_.findKey(modelCtrl.$$parentForm, function(otherModelCtrl) {
return otherModelCtrl && otherModelCtrl.hasOwnProperty('$modelValue') && otherModelCtrl !== modelCtrl && otherModelCtrl.$dirty;
});
if (!oneDirty) {
modelCtrl.$$parentForm.$setPristine(true);
}
}
I am using FabricJS with an AngularJS application. I am able to add text to a canvas and, using the kitchensink example located here, I can perform functions such as bold, italic, underline, etc.
However, the issue I have is how to change the font family, text align, font size, etc. since when I make a selection from a dropdown for font family, no changes occur... but it works in the Kitchensink example.
I am using the Kitchensink example as I need to not only add text, but edit it once it shows on the Canvas, and this appears to have what I need.
A button (which is working) has an HTML element such as:
<button class="btn btn-object-action" type="button" ng-class="{'btn-inverse': isBold()}" ng-click="toggleBold()">
Bold</button>
Which is backed up by the following in the Controller:
$scope.toggleBold = function () {
setActiveStyle('fontWeight',
getActiveStyle('fontWeight') === 'bold' ? '' : 'bold');
};
As I stated, this works as intended. Where I am having challenges is changing something like the Font Family or Font Size in that the change is achieved without a button click. Here is sample HTML for the Font Family select from the Kitchensink example:
<label style="display: inline-block;" for="font-family">Font family:</label><select class="btn-object-action" id="font-family" bind-value-to="fontFamily">
<option value="arial">Arial</option>
<option value="helvetica" selected="">Helvetica</option>
<option value="myriad pro">Myriad Pro</option>
</select>
This is backed up by this in the controller:
function getActiveProp(name) {
var object = canvas.getActiveObject();
if (!object) return '';
return object[name] || '';
}
function setActiveProp(name, value) {
var object = canvas.getActiveObject();
if (!object) return;
object.set(name, value).setCoords();
canvas.renderAll();
}
$scope.getFontFamily = function () {
return getActiveProp('fontFamily').toLowerCase();
};
$scope.setFontFamily = function (value) {
setActiveProp('fontFamily', value.toLowerCase());
};
function watchCanvas($scope) {
function updateScope() {
$scope.$$phase || $scope.$digest();
canvas.renderAll();
}
canvas
.on('object:selected', updateScope)
.on('group:selected', updateScope)
.on('path:created', updateScope)
.on('selection:cleared', updateScope);
}
$scope.getSelected = function () {
return canvas.getActiveObject();
};
$scope.canvas = canvas;
$scope.getActiveStyle = getActiveStyle;
addAccessors($scope);
watchCanvas($scope);
I am more used to using ng-model than bind-value-to, in fact, I have never seen or used bind-value-to before in Angular apps/not sure how and if I should be using it.
My main question is how do I get the dropdowns working where if I select a value it updates per the Kitchensink example for text here? What I am missing/is there a better way given that my need is to add and edit stylized text.
You need to bind a change event to your select element so that when you change the select value, you execute the function that will change the font family.
bind-value-to is a custom directive in the kitchensink app so don't worry about it.
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?
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.
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'