How to scrollTop a div whose content is managed by

2020-04-17 05:36发布

问题:

I have a div holding some chat history. I would like for the div content to scroll when it becomes full. I have this working using jQuery in another project, but what would be the proper AngularDart way to achieve this?

Simplifying a bit, in my HTML I have

<div class="chatHistory">{{ctrl.text}}</div>

(using style white-space:pre) and in my controller I have the method

void addToChatHistory(String msg) {
  text += msg;
  var elt = querySelector(".chatHistory");
  elt.scrollTop = elt.scrollHeight;
}

Issues:

  • This code scrolls the div content but not quite enough because it sets the scrollTop too quickly; i.e., even before the .chatHistory can be updated by Angular.
  • Besides this (partial) solution doesn't feel very Angular in style given the use of querySelector.

I tried setting up a watch on the text field but that also fires too early. Suggestions?

回答1:

For the first issue you can use Timer.run to defer the scroll execution :

Timer.run(() => elt.scrollTop = elt.scrollHeight);

For the second issue you can inject the Element managed by your controller and use querySelector on it :

MyController(Element e) : elt = e.querySelector('.chatHistory');


回答2:

EDIT

I published a package containing this directive: http://pub.dartlang.org/packages/bwu_angular
-------

I would create a Directive/Decorator like NgEventDirective but for the resize event and add it to your div. In the event handler you set your scrollTop property.

I found contradictory info about the resize event.

  • DART - Resize div element event says it is available everywhere
  • Onresize for div elements? is a workaround for browsers that don't support this event.

  • another page covering this topic: http://marcj.github.io/css-element-queries/

I tried to create an Angular implementation for the 'workaround' (2nd link) but run into this issue https://code.google.com/p/dart/issues/detail?id=18062 which contains info about a workaround but I didn't yet find time to implement it.

EDIT

I checked my attempt and it worked in Dartium with the version I downloaded today (Dart VM version: 1.4.0-dev.4.0 (Thu May 1 04:06:09 2014) on "linux_x64"). I haven't tried in other version since I created the issue.

(The code should be improved to make the directive more generic - the event method should be assignable by an attribute not hardcoded)

index.html

<!DOCTYPE html>
<html ng-app>
  <head>
    <script src="packages/web_components/platform.js"></script>
    <style>
      .resize-triggers {
              visibility: hidden;
      }

      .resize-triggers, .resize-triggers > div, .contract-trigger:before {
        content: " ";
        display: block;
        position: absolute;
        top: 0;
        left: 0;
        height: 100%;
        width: 100%;
        overflow: hidden;
      }

      .resize-triggers > div {
        background: #eee;
        overflow: auto;
      }

      .contract-trigger:before {
        width: 200%;
        height: 200%;
      }
    </style>
    <style>
      #my_element {
        height: 200px;
        overflow: scroll;
      }
    </style>
  </head>
  <body ng-cloak>
    <div>
      <div id="my_element"  style="border: 1px solid blue;">
        <div id='my_sizable' ng-observe-size></div>
      </div>
    </div>

    <script type="application/dart" src="index.dart"></script>
    <script type="text/javascript" src="packages/browser/dart.js"></script>

  </body>
</html>

index.dart

library angular_observe_resize.main;

import 'dart:async' as async;
import 'dart:html' as dom;
import 'package:angular/angular.dart' as ng;
import 'package:angular/application_factory.dart' as ngaf;

// see https://stackoverflow.com/questions/19329530
// and this bug https://code.google.com/p/dart/issues/detail?id=18062
// source from http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/

@ng.Decorator(selector: '[ng-observe-size]')
class NgObserveSizeDirective implements ng.AttachAware, ng.DetachAware {
  dom.Element _element;
  bool _hasAttacheEvent;

  NgObserveSizeDirective(this._element);

  void onSizeChange(dom.Event e) {
    _element.parent.scrollTop = _element.scrollHeight;
  }

  dom.HtmlElement _triggers;

  void resetTriggers() {
    var expand = _triggers.children[0];
    var contract = _triggers.children[_triggers.children.length - 1];
    var expandChild = expand.children[0];
    contract.scrollLeft = contract.scrollWidth;
    contract.scrollTop = contract.scrollHeight;
    expandChild.style.width = '${expand.offsetWidth + 1}px';
    expandChild.style.height = '${expand.offsetHeight + 1}px';
    expand.scrollLeft = expand.scrollWidth;
    expand.scrollTop = expand.scrollHeight;
  }

  int _resizeLastWidth;
  int _resizeLastHeight;

  bool checkTriggers() {
    return _element.offsetWidth != _resizeLastWidth ||
        _element.offsetHeight != _resizeLastHeight;
  }

  int _resizeRaf;


  void scrollListener(dom.Event e) {
    resetTriggers();
    if(_resizeRaf != null) {
      dom.window.cancelAnimationFrame(_resizeRaf);
    }
    _resizeRaf = dom.window.requestAnimationFrame((num highResTime){
      if(checkTriggers()) {
        _resizeLastWidth = _element.offsetWidth;
        _resizeLastHeight = _element.offsetHeight;
        onSizeChange(e);
      }
    });
  }

  @override
  void attach() {
    if(_element.getComputedStyle().position == 'static') {
      _element.style.position = 'relative';
    }

    _triggers = new dom.DivElement()
      ..classes.add('resize-triggers')
      ..append(new dom.DivElement()..classes.add('expand-trigger')..append(new dom.DivElement()))
      ..append(new dom.DivElement()..classes.add('contract-trigger'));
   _element.append(_triggers);

    new async.Future.delayed(new Duration(seconds: 1), () {
      //_triggers = _element.children[_element.children.length - 1];
      resetTriggers();

      dom.Element.scrollEvent.forTarget(_element, useCapture: true).listen(scrollListener);
    });
  }

  @override
  void detach() {
    _triggers.remove();
  }
}

class MyAppModule extends ng.Module {
  MyAppModule() {
    type(NgObserveSizeDirective);
  }
}

main() {
  print('main');
  ngaf.applicationFactory().addModule(new MyAppModule()).run();

  new async.Timer.periodic(new Duration(seconds: 1), (t) {
    var elt = (dom.querySelector('#my_sizable') as dom.HtmlElement);
    elt.append(new dom.Element.html('<div>${new DateTime.now()}</div>'));
  });
}


回答3:

If you're using an AngularDart component you can inject the shadowdom then select an element inside it to focus on. Didnt find a way of getting the container of the shadowdom.

new Future.delayed(new Duration(milliseconds: 300), () {
  var rowElement = _shadowRoot.querySelector('.step-container');
  if (rowElement != null ) {
    rowElement.scrollIntoView(ScrollAlignment.TOP);
  }
});

ShadowDom.host might work, though its currently marked as experimental.