diff options
-rw-r--r-- | webclient/app-directive.js | 43 | ||||
-rwxr-xr-x | webclient/app.css | 31 | ||||
-rw-r--r-- | webclient/app.js | 3 | ||||
-rw-r--r-- | webclient/components/matrix/event-handler-service.js | 7 | ||||
-rw-r--r-- | webclient/index.html | 1 | ||||
-rw-r--r-- | webclient/js/elastic.js | 216 | ||||
-rw-r--r-- | webclient/room/room-controller.js | 31 | ||||
-rw-r--r-- | webclient/room/room.html | 47 |
8 files changed, 356 insertions, 23 deletions
diff --git a/webclient/app-directive.js b/webclient/app-directive.js index 75283598ab..c1ba0af3a9 100644 --- a/webclient/app-directive.js +++ b/webclient/app-directive.js @@ -40,4 +40,45 @@ angular.module('matrixWebClient') } } }; -}]); \ No newline at end of file +}]) +.directive('asjson', function() { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, element, attrs, ngModelCtrl) { + function isValidJson(model) { + var flag = true; + try { + angular.fromJson(model); + } catch (err) { + flag = false; + } + return flag; + }; + + function string2JSON(text) { + try { + var j = angular.fromJson(text); + ngModelCtrl.$setValidity('json', true); + return j; + } catch (err) { + //returning undefined results in a parser error as of angular-1.3-rc.0, and will not go through $validators + //return undefined + ngModelCtrl.$setValidity('json', false); + return text; + } + }; + + function JSON2String(object) { + return angular.toJson(object, true); + }; + + //$validators is an object, where key is the error + //ngModelCtrl.$validators.json = isValidJson; + + //array pipelines + ngModelCtrl.$parsers.push(string2JSON); + ngModelCtrl.$formatters.push(JSON2String); + } + } +}); diff --git a/webclient/app.css b/webclient/app.css index 20a13aad81..5ab8e2b8fd 100755 --- a/webclient/app.css +++ b/webclient/app.css @@ -418,6 +418,37 @@ textarea, input { margin-top: 15px; } +/*** Room Info Dialog ***/ + +.room-info { + border-collapse: collapse; + width: 100%; +} + +.room-info-event { + border-bottom: 1pt solid black; +} + +.room-info-event-meta { + padding-top: 1em; + padding-bottom: 1em; +} + +.room-info-event-content { + padding-top: 1em; + padding-bottom: 1em; +} + +.monospace { + font-family: monospace; +} + +.room-info-textarea-content { + height: auto; + width: 100%; + resize: vertical; +} + /*** Participant list ***/ #usersTableWrapper { diff --git a/webclient/app.js b/webclient/app.js index 099e2170a0..8d9b662ee9 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -31,7 +31,8 @@ var matrixWebClient = angular.module('matrixWebClient', [ 'eventStreamService', 'eventHandlerService', 'infinite-scroll', - 'ui.bootstrap' + 'ui.bootstrap', + 'monospaced.elastic' ]); matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js index 3b1354cdef..6f251eec56 100644 --- a/webclient/components/matrix/event-handler-service.js +++ b/webclient/components/matrix/event-handler-service.js @@ -564,6 +564,13 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) { handleRedaction(event, isLiveEvent); break; default: + // if it is a state event, then just add it in so it + // displays on the Room Info screen. + if (typeof(event.state_key) === "string") { // incls. 0-len strings + if (event.room_id) { + handleRoomDateEvent(event, isLiveEvent, false); + } + } console.log("Unable to handle event type " + event.type); console.log(JSON.stringify(event, undefined, 4)); break; diff --git a/webclient/index.html b/webclient/index.html index 35c8051298..d8b9c95353 100644 --- a/webclient/index.html +++ b/webclient/index.html @@ -20,6 +20,7 @@ <script type='text/javascript' src="js/ui-bootstrap-tpls-0.11.2.js"></script> <script type='text/javascript' src='js/ng-infinite-scroll-matrix.js'></script> <script type='text/javascript' src='js/autofill-event.js'></script> + <script type='text/javascript' src='js/elastic.js'></script> <script src="app.js"></script> <script src="config.js"></script> <script src="app-controller.js"></script> diff --git a/webclient/js/elastic.js b/webclient/js/elastic.js new file mode 100644 index 0000000000..d585d81109 --- /dev/null +++ b/webclient/js/elastic.js @@ -0,0 +1,216 @@ +/* + * angular-elastic v2.4.0 + * (c) 2014 Monospaced http://monospaced.com + * License: MIT + */ + +angular.module('monospaced.elastic', []) + + .constant('msdElasticConfig', { + append: '' + }) + + .directive('msdElastic', [ + '$timeout', '$window', 'msdElasticConfig', + function($timeout, $window, config) { + 'use strict'; + + return { + require: 'ngModel', + restrict: 'A, C', + link: function(scope, element, attrs, ngModel) { + + // cache a reference to the DOM element + var ta = element[0], + $ta = element; + + // ensure the element is a textarea, and browser is capable + if (ta.nodeName !== 'TEXTAREA' || !$window.getComputedStyle) { + return; + } + + // set these properties before measuring dimensions + $ta.css({ + 'overflow': 'hidden', + 'overflow-y': 'hidden', + 'word-wrap': 'break-word' + }); + + // force text reflow + var text = ta.value; + ta.value = ''; + ta.value = text; + + var append = attrs.msdElastic ? attrs.msdElastic.replace(/\\n/g, '\n') : config.append, + $win = angular.element($window), + mirrorInitStyle = 'position: absolute; top: -999px; right: auto; bottom: auto;' + + 'left: 0; overflow: hidden; -webkit-box-sizing: content-box;' + + '-moz-box-sizing: content-box; box-sizing: content-box;' + + 'min-height: 0 !important; height: 0 !important; padding: 0;' + + 'word-wrap: break-word; border: 0;', + $mirror = angular.element('<textarea tabindex="-1" ' + + 'style="' + mirrorInitStyle + '"/>').data('elastic', true), + mirror = $mirror[0], + taStyle = getComputedStyle(ta), + resize = taStyle.getPropertyValue('resize'), + borderBox = taStyle.getPropertyValue('box-sizing') === 'border-box' || + taStyle.getPropertyValue('-moz-box-sizing') === 'border-box' || + taStyle.getPropertyValue('-webkit-box-sizing') === 'border-box', + boxOuter = !borderBox ? {width: 0, height: 0} : { + width: parseInt(taStyle.getPropertyValue('border-right-width'), 10) + + parseInt(taStyle.getPropertyValue('padding-right'), 10) + + parseInt(taStyle.getPropertyValue('padding-left'), 10) + + parseInt(taStyle.getPropertyValue('border-left-width'), 10), + height: parseInt(taStyle.getPropertyValue('border-top-width'), 10) + + parseInt(taStyle.getPropertyValue('padding-top'), 10) + + parseInt(taStyle.getPropertyValue('padding-bottom'), 10) + + parseInt(taStyle.getPropertyValue('border-bottom-width'), 10) + }, + minHeightValue = parseInt(taStyle.getPropertyValue('min-height'), 10), + heightValue = parseInt(taStyle.getPropertyValue('height'), 10), + minHeight = Math.max(minHeightValue, heightValue) - boxOuter.height, + maxHeight = parseInt(taStyle.getPropertyValue('max-height'), 10), + mirrored, + active, + copyStyle = ['font-family', + 'font-size', + 'font-weight', + 'font-style', + 'letter-spacing', + 'line-height', + 'text-transform', + 'word-spacing', + 'text-indent']; + + // exit if elastic already applied (or is the mirror element) + if ($ta.data('elastic')) { + return; + } + + // Opera returns max-height of -1 if not set + maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4; + + // append mirror to the DOM + if (mirror.parentNode !== document.body) { + angular.element(document.body).append(mirror); + } + + // set resize and apply elastic + $ta.css({ + 'resize': (resize === 'none' || resize === 'vertical') ? 'none' : 'horizontal' + }).data('elastic', true); + + /* + * methods + */ + + function initMirror() { + var mirrorStyle = mirrorInitStyle; + + mirrored = ta; + // copy the essential styles from the textarea to the mirror + taStyle = getComputedStyle(ta); + angular.forEach(copyStyle, function(val) { + mirrorStyle += val + ':' + taStyle.getPropertyValue(val) + ';'; + }); + mirror.setAttribute('style', mirrorStyle); + } + + function adjust() { + var taHeight, + taComputedStyleWidth, + mirrorHeight, + width, + overflow; + + if (mirrored !== ta) { + initMirror(); + } + + // active flag prevents actions in function from calling adjust again + if (!active) { + active = true; + + mirror.value = ta.value + append; // optional whitespace to improve animation + mirror.style.overflowY = ta.style.overflowY; + + taHeight = ta.style.height === '' ? 'auto' : parseInt(ta.style.height, 10); + + taComputedStyleWidth = getComputedStyle(ta).getPropertyValue('width'); + + // ensure getComputedStyle has returned a readable 'used value' pixel width + if (taComputedStyleWidth.substr(taComputedStyleWidth.length - 2, 2) === 'px') { + // update mirror width in case the textarea width has changed + width = parseInt(taComputedStyleWidth, 10) - boxOuter.width; + mirror.style.width = width + 'px'; + } + + mirrorHeight = mirror.scrollHeight; + + if (mirrorHeight > maxHeight) { + mirrorHeight = maxHeight; + overflow = 'scroll'; + } else if (mirrorHeight < minHeight) { + mirrorHeight = minHeight; + } + mirrorHeight += boxOuter.height; + + ta.style.overflowY = overflow || 'hidden'; + + if (taHeight !== mirrorHeight) { + ta.style.height = mirrorHeight + 'px'; + scope.$emit('elastic:resize', $ta); + } + + // small delay to prevent an infinite loop + $timeout(function() { + active = false; + }, 1); + + } + } + + function forceAdjust() { + active = false; + adjust(); + } + + /* + * initialise + */ + + // listen + if ('onpropertychange' in ta && 'oninput' in ta) { + // IE9 + ta['oninput'] = ta.onkeyup = adjust; + } else { + ta['oninput'] = adjust; + } + + $win.bind('resize', forceAdjust); + + scope.$watch(function() { + return ngModel.$modelValue; + }, function(newValue) { + forceAdjust(); + }); + + scope.$on('elastic:adjust', function() { + initMirror(); + forceAdjust(); + }); + + $timeout(adjust); + + /* + * destroy + */ + + scope.$on('$destroy', function() { + $mirror.remove(); + $win.unbind('resize', forceAdjust); + }); + } + }; + } + ]); diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index 37f51c4e91..59274baccb 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -1018,6 +1018,20 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) }; $scope.openRoomInfo = function() { + $scope.roomInfo = {}; + $scope.roomInfo.newEvent = { + content: {}, + type: "", + state_key: "" + }; + + var stateFilter = $filter("stateEventsFilter"); + var stateEvents = stateFilter($scope.events.rooms[$scope.room_id]); + // The modal dialog will 2-way bind this field, so we MUST make a deep + // copy of the state events else we will be *actually adjusing our view + // of the world* when fiddling with the JSON!! Apparently parse/stringify + // is faster than jQuery's extend when doing deep copies. + $scope.roomInfo.stateEvents = JSON.parse(JSON.stringify(stateEvents)); var modalInstance = $modal.open({ templateUrl: 'roomInfoTemplate.html', controller: 'RoomInfoController', @@ -1036,12 +1050,21 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) $modalInstance.close("redact"); }; }) -.controller('RoomInfoController', function($scope, $modalInstance, $filter) { +.controller('RoomInfoController', function($scope, $modalInstance, $filter, matrixService) { console.log("Displaying room info."); - $scope.submitState = function(eventType, content) { - console.log("Submitting " + eventType + " with " + content); - } + $scope.submit = function(event) { + if (event.content) { + console.log("submit >>> " + JSON.stringify(event.content)); + matrixService.sendStateEvent($scope.room_id, event.type, + event.content, event.state_key).then(function(response) { + $modalInstance.dismiss(); + }, function(err) { + $scope.feedback = err.data.error; + } + ); + } + }; $scope.dismiss = $modalInstance.dismiss; diff --git a/webclient/room/room.html b/webclient/room/room.html index 3458e97039..fac7433a4b 100644 --- a/webclient/room/room.html +++ b/webclient/room/room.html @@ -15,23 +15,36 @@ <script type="text/ng-template" id="roomInfoTemplate.html"> <div class="modal-body"> - <table id="roomInfoTable"> - <tr> - <th> - Event Type - </th> - <th> - Content - </th> - </tr> - <tr ng-repeat="(key, event) in events.rooms[room_id] | stateEventsFilter"> - <td> - <pre>{{ key }}</pre> - </td> - <td> - <pre>{{ event.content | json }}</pre> - </td> - </tr> + <table class="room-info"> + <tr ng-repeat="(key, event) in roomInfo.stateEvents" class="room-info-event"> + <td class="room-info-event-meta" width="30%"> + <span class="monospace">{{ key }}</span> + <br/> + {{ (event.origin_server_ts) | date:'MMM d HH:mm' }} + <br/> + Set by: <span class="monospace">{{ event.user_id }}</span> + <br/> + <span ng-show="event.required_power_level >= 0">Required power level: {{event.required_power_level}}<br/></span> + <button ng-click="submit(event)" type="button" class="btn btn-success" ng-disabled="!event.content"> + Submit + </button> + </td> + <td class="room-info-event-content" width="70%"> + <textarea class="room-info-textarea-content" msd-elastic ng-model="event.content" asjson></textarea> + </td> + </tr> + <tr> + <td class="room-info-event-meta" width="30%"> + <input ng-model="roomInfo.newEvent.type" placeholder="your.event.type" /> + <br/> + <button ng-click="submit(roomInfo.newEvent)" type="button" class="btn btn-success" ng-disabled="!roomInfo.newEvent.content || !roomInfo.newEvent.type"> + Submit + </button> + </td> + <td class="room-info-event-content" width="70%"> + <textarea class="room-info-textarea-content" msd-elastic ng-model="roomInfo.newEvent.content" asjson></textarea> + </td> + </tr> </table> </div> <div class="modal-footer"> |