├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── angular-resizable.min.css ├── angular-resizable.min.js ├── bower.json ├── demo.gif ├── gulpfile.js ├── package.json └── src ├── angular-resizable.css └── angular-resizable.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | 39 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | gulpfile.js 2 | demo.gif 3 | .gitignore 4 | bower.json 5 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jacob 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-resizable 2 | A directive for creating resizable containers. 3 | 4 | ## Why? 5 | All other resizable directive concepts I came across include layout logic in the directive. I wanted a directive that only handled the resize logic. This way, the layout logic is quarantined to the CSS. 6 | 7 | ## Usage 8 | 9 | 1. `npm install angular-resizable` or `bower install angular-resizable` or clone/download this repo 10 | 2. Include `angular-resizable.min.js` in your project. 11 | 3. Include `angular-resizable.min.css` in your project as well (this provides default styling for the resize handles). 12 | 4. Then include the module in your app: `angular.module('app', ['angularResizable'])` 13 | 5. Use it: `
` 14 | 15 | Include any sides you want to be resizable in an array inside `r-directions`. Accepts 'top','right','bottom', and 'left'. You can style the handles however you want. Just override the styles in the css in your own stylesheet. 16 | 17 | ## Options 18 | 19 | Attributes | Default | Accepts | Description 20 | --- | --- | --- | --- 21 | rDirections | ['right'] | ['top', 'right', 'bottom', 'left',] | Determines which sides of the element are resizable. 22 | rFlex | false | boolean | Set as true if you are using flexbox. [See this codepen](http://codepen.io/Reklino/pen/raRaXq). 23 | rCenteredX | false | boolean | If set as true, the velocity of horizontal resizing will be doubled. 24 | rCenteredY | false | boolean | If set as true, the velocity of vertical resizing will be doubled. 25 | rWidth | false | integer or $scope variable | If set, the resizable element will be rendered with a predefined width relative to this value in pixels and a watcher will be set on the 'rWidth' attribute. [See this codepen](http://codepen.io/Reklino/pen/EjKXqg). 26 | rHeight | false | integer or $scope variable | If set, the resizable element will be rendered with a predefined height relative to this value in pixels and a watcher will be set on the 'rHeight' attribute. [See this codepen](http://codepen.io/Reklino/pen/EjKXqg). 27 | rGrabber | `` | string | Defines custom inner html for the grabber. 28 | rNoThrottle | false | boolean | Disables `angular-resizable.resizing` throttling (see events section below). 29 | 30 | ## Events 31 | 32 | For an example using the events, [see this codepen](http://codepen.io/Reklino/pen/EjKXqg). 33 | 34 | ### angular-resizable.resizeStart 35 | 36 | This event is emitted at the beginning of a resize with the following info object: 37 | - `info.width` : The width of the directive at time of resize start. **Will be false if resizing vertically** 38 | - `info.height` : The height of the directive at time of resize start. **Will be false if resizing horizontally** 39 | - `info.id` : The id of the directive. **Will be false if there is no id set.** 40 | - `info.evt` : original mouse event object 41 | 42 | ### angular-resizable.resizing 43 | 44 | Called repeatedly while the mouse is being moved. By default, only calls once every 100ms to keep CPU usage low. For smooth resizing, use the `rNoThrottle` attribute. This event is emitted during the resizing of the element with the following object as an argument: 45 | - `info.width` : The width of the directive at time of resize end. **Will be false if resizing vertically** 46 | - `info.height` : The height of the directive at time of resize end. **Will be false if resizing horizontally** 47 | - `info.id` : The id of the directive. **Will be false if there is no id set.** 48 | - `info.evt` : original mouse event object 49 | 50 | ### angular-resizable.resizeEnd 51 | 52 | This event is emitted at the end of a resize with the following object as an argument: 53 | - `info.width` : The width of the directive at time of resize end. **Will be false if resizing vertically** 54 | - `info.height` : The height of the directive at time of resize end. **Will be false if resizing horizontally** 55 | - `info.id` : The id of the directive. **Will be false if there is no id set.** 56 | - `info.evt` : original mouse event object 57 | 58 | ## version notes 59 | 60 | ### 1.2.0 61 | - Add angular-resizable.resizing event (see pull request #7) 62 | - Add attribute for providing custom inner html to the grabber element (see pull request #7) 63 | 64 | ## License 65 | 66 | MIT 67 | -------------------------------------------------------------------------------- /angular-resizable.min.css: -------------------------------------------------------------------------------- 1 | .resizable{position:relative}.resizable.no-transition{transition:none!important}.rg-bottom,.rg-left,.rg-right,.rg-top{display:block;width:14px;height:14px;line-height:14px;position:absolute;z-index:1;-moz-user-select:-moz-none;-ms-user-select:none;-webkit-user-select:none;user-select:none;background:0 0}.rg-bottom span,.rg-left span,.rg-right span,.rg-top span{position:absolute;box-sizing:border-box;display:block;border:1px solid #ccc}.rg-left span,.rg-right span{border-width:0 1px;top:50%;margin:-10px 0 0 3.5px;height:20px;width:7px}.rg-bottom span,.rg-top span{border-width:1px 0;left:50%;margin:3.5px 0 0 -10px;width:20px;height:7px}.rg-top{cursor:row-resize;width:100%;top:0;left:0;margin-top:-14px}.rg-right{cursor:col-resize;height:100%;right:0;top:0;margin-right:-14px}.rg-bottom{cursor:row-resize;width:100%;bottom:0;left:0;margin-bottom:-14px}.rg-left{cursor:col-resize;height:100%;left:0;top:0;margin-left:-14px} -------------------------------------------------------------------------------- /angular-resizable.min.js: -------------------------------------------------------------------------------- 1 | angular.module("angularResizable",[]).directive("resizable",function(){function e(e){void 0===t?(t=e,setTimeout(function(){t(),t=void 0},100)):t=e}var t;return{restrict:"AE",scope:{rDirections:"=",rCenteredX:"=",rCenteredY:"=",rWidth:"=",rHeight:"=",rFlex:"=",rGrabber:"@",rDisabled:"@"},link:function(t,n,r){var i="flexBasis"in document.documentElement.style?"flexBasis":"webkitFlexBasis"in document.documentElement.style?"webkitFlexBasis":"msFlexPreferredSize"in document.documentElement.style?"msFlexPreferredSize":"flexBasis";t.$watch("rWidth",function(e){n[0].style[t.rFlex?i:"width"]=t.rWidth+"px"}),t.$watch("rHeight",function(e){n[0].style[t.rFlex?i:"height"]=t.rHeight+"px"}),n.addClass("resizable");var s,o,a,u,l,d=window.getComputedStyle(n[0],null),c=t.rDirections,h=t.rCenteredX?2:1,m=t.rCenteredY?2:1,v=t.rGrabber?t.rGrabber:"",p={},f=function(e){p.width=!1,p.height=!1,"x"===l?p.width=parseInt(n[0].style[t.rFlex?i:"width"]):p.height=parseInt(n[0].style[t.rFlex?i:"height"]),p.id=n[0].id,p.evt=e},x=function(e){return e.touches?e.touches[0].clientX:e.clientX},g=function(e){return e.touches?e.touches[0].clientY:e.clientY},b=function(r){var d,c="x"===l?a-x(r):a-g(r);switch(u){case"top":d=t.rFlex?i:"height",n[0].style[d]=o+c*m+"px";break;case"bottom":d=t.rFlex?i:"height",n[0].style[d]=o-c*m+"px";break;case"right":d=t.rFlex?i:"width",n[0].style[d]=s-c*h+"px";break;case"left":d=t.rFlex?i:"width",n[0].style[d]=s+c*h+"px"}f(r),e(function(){t.$emit("angular-resizable.resizing",p)})},y=function(e){f(),t.$emit("angular-resizable.resizeEnd",p),t.$apply(),document.removeEventListener("mouseup",y,!1),document.removeEventListener("mousemove",b,!1),document.removeEventListener("touchend",y,!1),document.removeEventListener("touchmove",b,!1),n.removeClass("no-transition")},E=function(e,r){u=r,l="left"===u||"right"===u?"x":"y",a="x"===l?x(e):g(e),s=parseInt(d.getPropertyValue("width")),o=parseInt(d.getPropertyValue("height")),n.addClass("no-transition"),document.addEventListener("mouseup",y,!1),document.addEventListener("mousemove",b,!1),document.addEventListener("touchend",y,!1),document.addEventListener("touchmove",b,!1),e.stopPropagation&&e.stopPropagation(),e.preventDefault&&e.preventDefault(),e.cancelBubble=!0,e.returnValue=!1,f(e),t.$emit("angular-resizable.resizeStart",p),t.$apply()};c.forEach(function(e){var r=document.createElement("div");r.setAttribute("class","rg-"+e),r.innerHTML=v,n[0].appendChild(r),r.ondragstart=function(){return!1};var i=function(n){var r="true"===t.rDisabled;r||1!==n.which&&!n.touches||E(n,e)};r.addEventListener("mousedown",i,!1),r.addEventListener("touchstart",i,!1)})}}}); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-resizable", 3 | "version": "1.2.0", 4 | "homepage": "https://github.com/Reklino/angular-resizable", 5 | "authors": [ 6 | "Reklino " 7 | ], 8 | "description": "A lightweight directive for creating resizable containers in angularjs", 9 | "main": "src/angular-resizable.js", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests", 17 | "demo.gif", 18 | ".gitignore", 19 | "package.json", 20 | ".npmignore", 21 | "gulpfile.js" 22 | ], 23 | "dependencies": { 24 | "angular": "~1.*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reklino/angular-resizable/6b8769fddcc05fdaaaef5f2ef57147de5f556ddf/demo.gif -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var uglify = require('gulp-uglify'); 3 | var minify = require('gulp-minify-css'); 4 | var rename = require("gulp-rename"); 5 | 6 | gulp.task('js', function() { 7 | return gulp.src('src/*.js') 8 | .pipe(uglify()) 9 | .pipe(rename(function(path) { 10 | path.basename += ".min" 11 | })) 12 | .pipe(gulp.dest('')); 13 | }); 14 | 15 | gulp.task('css', function() { 16 | return gulp.src('src/*.css') 17 | .pipe(minify()) 18 | .pipe(rename(function(path) { 19 | path.basename += ".min" 20 | })) 21 | .pipe(gulp.dest('')); 22 | }); 23 | 24 | gulp.task('default', ['js', 'css']); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-resizable", 3 | "version": "1.2.0", 4 | "description": "A directive for creating resizable containers in angular.", 5 | "keywords": [ 6 | "angular", 7 | "angular-js", 8 | "angularjs", 9 | "resize", 10 | "resizable", 11 | "directive", 12 | "container", 13 | "lightweight" 14 | ], 15 | "main": "src/angular-resizable.js", 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/Reklino/angular-resizable.git" 22 | }, 23 | "author": "Reklino", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/Reklino/angular-resizable/issues" 27 | }, 28 | "homepage": "https://github.com/Reklino/angular-resizable", 29 | "devDependencies": { 30 | "gulp": "^3.8.11", 31 | "gulp-minify-css": "^1.1.1", 32 | "gulp-rename": "^1.2.2", 33 | "gulp-uglify": "^1.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/angular-resizable.css: -------------------------------------------------------------------------------- 1 | .resizable { 2 | position: relative; 3 | } 4 | .resizable.no-transition { 5 | transition: none !important; 6 | } 7 | 8 | .rg-right, .rg-left, .rg-top, .rg-bottom { 9 | display: block; 10 | width: 14px; 11 | height: 14px; 12 | line-height: 14px; 13 | position: absolute; 14 | z-index: 1; 15 | -moz-user-select: -moz-none; 16 | -ms-user-select: none; 17 | -webkit-user-select: none; 18 | user-select: none; 19 | background: transparent; 20 | } 21 | .rg-right span, .rg-left span, .rg-top span, .rg-bottom span { 22 | position: absolute; 23 | box-sizing: border-box; 24 | display: block; 25 | border: 1px solid #ccc; 26 | } 27 | 28 | .rg-right span, .rg-left span { 29 | border-width: 0 1px; 30 | top: 50%; 31 | margin-top: -10px; 32 | margin: -10px 0 0 3.5px; 33 | height: 20px; 34 | width: 7px; 35 | } 36 | 37 | .rg-top span, .rg-bottom span { 38 | border-width: 1px 0; 39 | left: 50%; 40 | margin: 3.5px 0 0 -10px; 41 | width: 20px; 42 | height: 7px; 43 | } 44 | 45 | .rg-top { 46 | cursor: row-resize; 47 | width: 100%; 48 | top: 0; 49 | left: 0; 50 | margin-top: -14px; 51 | } 52 | 53 | .rg-right { 54 | cursor: col-resize; 55 | height: 100%; 56 | right: 0; 57 | top: 0; 58 | margin-right: -14px; 59 | } 60 | 61 | .rg-bottom { 62 | cursor: row-resize; 63 | width: 100%; 64 | bottom: 0; 65 | left: 0; 66 | margin-bottom: -14px; 67 | } 68 | 69 | .rg-left { 70 | cursor: col-resize; 71 | height: 100%; 72 | left: 0; 73 | top: 0; 74 | margin-left: -14px; 75 | } -------------------------------------------------------------------------------- /src/angular-resizable.js: -------------------------------------------------------------------------------- 1 | angular.module('angularResizable', []) 2 | .directive('resizable', function() { 3 | var toCall; 4 | function throttle(fun) { 5 | if (toCall === undefined) { 6 | toCall = fun; 7 | setTimeout(function() { 8 | toCall(); 9 | toCall = undefined; 10 | }, 100); 11 | } else { 12 | toCall = fun; 13 | } 14 | } 15 | return { 16 | restrict: 'AE', 17 | scope: { 18 | rDirections: '=', 19 | rCenteredX: '=', 20 | rCenteredY: '=', 21 | rWidth: '=', 22 | rHeight: '=', 23 | rFlex: '=', 24 | rGrabber: '@', 25 | rDisabled: '@', 26 | rNoThrottle: '=', 27 | resizable: '@', 28 | }, 29 | link: function(scope, element, attr) { 30 | if (scope.resizable === 'false') return; 31 | 32 | var flexBasis = 'flexBasis' in document.documentElement.style ? 'flexBasis' : 33 | 'webkitFlexBasis' in document.documentElement.style ? 'webkitFlexBasis' : 34 | 'msFlexPreferredSize' in document.documentElement.style ? 'msFlexPreferredSize' : 'flexBasis'; 35 | 36 | // register watchers on width and height attributes if they are set 37 | scope.$watch('rWidth', function(value){ 38 | element[0].style[scope.rFlex ? flexBasis : 'width'] = scope.rWidth + 'px'; 39 | }); 40 | scope.$watch('rHeight', function(value){ 41 | element[0].style[scope.rFlex ? flexBasis : 'height'] = scope.rHeight + 'px'; 42 | }); 43 | 44 | element.addClass('resizable'); 45 | 46 | var style = window.getComputedStyle(element[0], null), 47 | w, 48 | h, 49 | dir = scope.rDirections || ['right'], 50 | vx = scope.rCenteredX ? 2 : 1, // if centered double velocity 51 | vy = scope.rCenteredY ? 2 : 1, // if centered double velocity 52 | inner = scope.rGrabber ? scope.rGrabber : '', 53 | start, 54 | dragDir, 55 | axis, 56 | info = {}; 57 | 58 | var updateInfo = function(e) { 59 | info.width = false; info.height = false; 60 | if(axis === 'x') 61 | info.width = parseInt(element[0].style[scope.rFlex ? flexBasis : 'width']); 62 | else 63 | info.height = parseInt(element[0].style[scope.rFlex ? flexBasis : 'height']); 64 | info.id = element[0].id; 65 | info.evt = e; 66 | }; 67 | 68 | var getClientX = function(e) { 69 | return e.touches ? e.touches[0].clientX : e.clientX; 70 | }; 71 | 72 | var getClientY = function(e) { 73 | return e.touches ? e.touches[0].clientY : e.clientY; 74 | }; 75 | 76 | var dragging = function(e) { 77 | var prop, offset = axis === 'x' ? start - getClientX(e) : start - getClientY(e); 78 | switch(dragDir) { 79 | case 'top': 80 | prop = scope.rFlex ? flexBasis : 'height'; 81 | element[0].style[prop] = h + (offset * vy) + 'px'; 82 | break; 83 | case 'bottom': 84 | prop = scope.rFlex ? flexBasis : 'height'; 85 | element[0].style[prop] = h - (offset * vy) + 'px'; 86 | break; 87 | case 'right': 88 | prop = scope.rFlex ? flexBasis : 'width'; 89 | element[0].style[prop] = w - (offset * vx) + 'px'; 90 | break; 91 | case 'left': 92 | prop = scope.rFlex ? flexBasis : 'width'; 93 | element[0].style[prop] = w + (offset * vx) + 'px'; 94 | break; 95 | } 96 | updateInfo(e); 97 | function resizingEmit(){ 98 | scope.$emit('angular-resizable.resizing', info); 99 | } 100 | if (scope.rNoThrottle) { 101 | resizingEmit(); 102 | } else { 103 | throttle(resizingEmit); 104 | } 105 | }; 106 | var dragEnd = function(e) { 107 | updateInfo(); 108 | scope.$emit('angular-resizable.resizeEnd', info); 109 | scope.$apply(); 110 | document.removeEventListener('mouseup', dragEnd, false); 111 | document.removeEventListener('mousemove', dragging, false); 112 | document.removeEventListener('touchend', dragEnd, false); 113 | document.removeEventListener('touchmove', dragging, false); 114 | element.removeClass('no-transition'); 115 | }; 116 | var dragStart = function(e, direction) { 117 | dragDir = direction; 118 | axis = dragDir === 'left' || dragDir === 'right' ? 'x' : 'y'; 119 | start = axis === 'x' ? getClientX(e) : getClientY(e); 120 | w = parseInt(style.getPropertyValue('width')); 121 | h = parseInt(style.getPropertyValue('height')); 122 | 123 | //prevent transition while dragging 124 | element.addClass('no-transition'); 125 | 126 | document.addEventListener('mouseup', dragEnd, false); 127 | document.addEventListener('mousemove', dragging, false); 128 | document.addEventListener('touchend', dragEnd, false); 129 | document.addEventListener('touchmove', dragging, false); 130 | 131 | // Disable highlighting while dragging 132 | if(e.stopPropagation) e.stopPropagation(); 133 | if(e.preventDefault) e.preventDefault(); 134 | e.cancelBubble = true; 135 | e.returnValue = false; 136 | 137 | updateInfo(e); 138 | scope.$emit('angular-resizable.resizeStart', info); 139 | scope.$apply(); 140 | }; 141 | 142 | dir.forEach(function (direction) { 143 | var grabber = document.createElement('div'); 144 | 145 | // add class for styling purposes 146 | grabber.setAttribute('class', 'rg-' + direction); 147 | grabber.innerHTML = inner; 148 | element[0].appendChild(grabber); 149 | grabber.ondragstart = function() { return false; }; 150 | 151 | var down = function(e) { 152 | var disabled = (scope.rDisabled === 'true'); 153 | if (!disabled && (e.which === 1 || e.touches)) { 154 | // left mouse click or touch screen 155 | dragStart(e, direction); 156 | } 157 | }; 158 | grabber.addEventListener('mousedown', down, false); 159 | grabber.addEventListener('touchstart', down, false); 160 | }); 161 | } 162 | }; 163 | }); 164 | --------------------------------------------------------------------------------