function addWindowListener(self, dir){

    const node = getByClass(self.dom, "wb-" + dir);
    if(!node) return;

    let touch, x, y;
    let raf_timer, raf_move, raf_resize;
    let dblclick_timer = 0;

    addListener(node, "mousedown", mousedown, eventOptions);
    addListener(node, "touchstart", mousedown, eventOptions);

    function loop(){

        raf_timer = requestAnimationFrame(loop);

        if(raf_resize){

            self.resize();
            raf_resize = false;
        }

        if(raf_move){

            self.move();
            raf_move = false;
        }
    }

    function mousedown(event){

        // prevent the full iteration through the fallback chain of a touch event (touch > mouse > click)
        preventEvent(event, true);
        //window_clicked = true;
        self.focus();

        if(dir === "drag"){

            if(self.min){

                self.restore();
                return;
            }

            if(!self.hasClass("no-max")){

                const now = Date.now();
                const diff = now - dblclick_timer;

                dblclick_timer = now;

                if(diff < 300){

                    self.max ? self.restore() : self.maximize();
                    return;
                }
            }
        }

        if(/*!self.max &&*/ !self.min){

            addClass(body, "wb-lock");
            use_raf && loop();

            if((touch = event.touches) && (touch = touch[0])){

                event = touch;

                // TODO: fix when touch events bubbles up to the document body
                //addListener(self.dom, "touchmove", preventEvent);
                addListener(window, "touchmove", handler_mousemove, eventOptionsPassive);
                addListener(window, "touchend", handler_mouseup, eventOptionsPassive);
            }
            else{

                //addListener(this, "mouseleave", handler_mouseup);
                addListener(window, "mousemove", handler_mousemove, eventOptionsPassive);
                addListener(window, "mouseup", handler_mouseup, eventOptionsPassive);
            }

            x = event.pageX;
            y = event.pageY;

            // appearing scrollbars on the root element does not trigger "window.onresize",
            // force refresh window size via init(), also force layout recalculation (layout trashing)
            // it is probably very rare that the body overflow changes between window open and close

            //init();
        }
    }

    function handler_mousemove(event){

        preventEvent(event);

        if(touch){

            event = event.touches[0];
        }

        const pageX = event.pageX;
        const pageY = event.pageY;
        const offsetX = pageX - x;
        const offsetY = pageY - y;

        const old_w = self.width;
        const old_h = self.height;
        const old_x = self.x;
        const old_y = self.y;

        let resize_w, resize_h, move_x, move_y;

        if(dir === "drag"){

            if(self.hasClass("no-move")) return;

            self.x += offsetX;
            self.y += offsetY;
            move_x = move_y = 1;
        }
        else{

            if(dir === "e" || dir === "se" || dir === "ne"){

                self.width += offsetX;
                resize_w = 1;
            }
            else if(dir === "w" || dir === "sw" || dir === "nw"){

                self.x += offsetX;
                self.width -= offsetX;
                resize_w = 1;
                move_x = 1;
            }

            if(dir === "s" || dir === "se" || dir === "sw"){

                self.height += offsetY;
                resize_h = 1;
            }
            else if(dir === "n" || dir === "ne" || dir === "nw"){

                self.y += offsetY;
                self.height -= offsetY;
                resize_h = 1;
                move_y = 1;
            }
        }

        if(resize_w){

            self.width = Math.max(Math.min(self.width, self.maxwidth, root_w - self.x - self.right), self.minwidth);
            resize_w = self.width !== old_w;
        }

        if(resize_h){

            self.height = Math.max(Math.min(self.height, self.maxheight, root_h - self.y - self.bottom), self.minheight);
            resize_h = self.height !== old_h;
        }

        if(resize_w || resize_h){

            use_raf ? raf_resize = true : self.resize();
        }

        if(move_x){

            if(self.max){

                self.x = (

                    pageX < root_w / 3 ?

                        self.left
                    :
                        pageX > root_w / 3 * 2 ?

                            root_w - self.width - self.right
                        :
                            root_w / 2 - self.width / 2

                ) + offsetX;
            }

            self.x = Math.max(Math.min(self.x, self.overflow ? root_w - 30 : root_w - self.width - self.right), self.overflow ? 30 - self.width : self.left);
            move_x = self.x !== old_x;
        }

        if(move_y){

            if(self.max){

                self.y = self.top + offsetY;
            }

            self.y = Math.max(Math.min(self.y, self.overflow ? root_h - self.header : root_h - self.height - self.bottom), self.top);
            move_y = self.y !== old_y;
        }

        if(move_x || move_y){

            if(self.max){

                self.restore();
            }

            use_raf ? raf_move = true : self.move();
        }

        if(resize_w || move_x){

            x = pageX;
        }

        if(resize_h || move_y){

            y = pageY;
        }
    }

    function handler_mouseup(event){

        preventEvent(event);
        removeClass(body, "wb-lock");
        use_raf && cancelAnimationFrame(raf_timer);

        if(touch){

            //removeListener(self.dom, "touchmove", preventEvent);
            removeListener(window, "touchmove", handler_mousemove, eventOptionsPassive);
            removeListener(window, "touchend", handler_mouseup, eventOptionsPassive);
        }
        else{

            //removeListener(this, "mouseleave", handler_mouseup);
            removeListener(window, "mousemove", handler_mousemove, eventOptionsPassive);
            removeListener(window, "mouseup", handler_mouseup, eventOptionsPassive);
        }
    }
}
@param {WinBox} self
@param {string} dir