From 6b0f7392372fd41c8b053d1a401bfadd2bb1ae87 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sun, 16 Sep 2012 15:42:31 +0200 Subject: [PATCH] Rewrite/refactor of UI. Use percents instead of pixels when dragging windows. (#16) Now supports touch and multi-touch for dragging windows. (#484) Boom. Boom. Close #16. Close #484. --- 4chan_x.user.js | 249 +++++++++++++++++++++++++++++++----------------- changelog | 1 + script.coffee | 203 +++++++++++++++++++++++---------------- 3 files changed, 286 insertions(+), 167 deletions(-) diff --git a/4chan_x.user.js b/4chan_x.user.js index 1b6c73a9d..1d5c73cc3 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -222,68 +222,157 @@ posts: {} }; - UI = { - dialog: function(id, position, html) { - var el; + UI = (function() { + var dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchmove; + dialog = function(id, position, html) { + var el, move; el = d.createElement('div'); el.className = 'reply dialog'; el.innerHTML = html; el.id = id; el.style.cssText = localStorage.getItem("" + g.NAMESPACE + id + ".position") || position; - el.querySelector('.move').addEventListener('mousedown', UI.dragstart, false); + move = el.querySelector('.move'); + move.addEventListener('touchstart', dragstart, false); + move.addEventListener('mousedown', dragstart, false); return el; - }, - dragstart: function(e) { - var el, rect; + }; + dragstart = function(e) { + var el, isTouching, o, rect, screenHeight, screenWidth; e.preventDefault(); - UI.el = el = this.parentNode; - d.addEventListener('mousemove', UI.drag, false); - d.addEventListener('mouseup', UI.dragend, false); - rect = el.getBoundingClientRect(); - UI.dx = e.clientX - rect.left; - UI.dy = e.clientY - rect.top; - UI.width = d.documentElement.clientWidth - rect.width; - return UI.height = d.documentElement.clientHeight - rect.height; - }, - drag: function(e) { - var left, style, top; - left = e.clientX - UI.dx; - top = e.clientY - UI.dy; - left = left < 10 ? '0px' : UI.width - left < 10 ? null : left + 'px'; - top = top < 10 ? '0px' : UI.height - top < 10 ? null : top + 'px'; - style = UI.el.style; - style.left = left; - style.top = top; - style.right = left ? null : '0px'; - return style.bottom = top ? null : '0px'; - }, - dragend: function() { - localStorage.setItem("" + g.NAMESPACE + UI.el.id + ".position", UI.el.style.cssText); - d.removeEventListener('mousemove', UI.drag, false); - d.removeEventListener('mouseup', UI.dragend, false); - return delete UI.el; - }, - hover: function(e) { - var clientHeight, clientWidth, clientX, clientY, height, style, top, _ref; - clientX = e.clientX, clientY = e.clientY; - style = UI.el.style; - _ref = d.documentElement, clientHeight = _ref.clientHeight, clientWidth = _ref.clientWidth; - height = UI.el.offsetHeight; - top = clientY - 120; - style.top = clientHeight <= height || top <= 0 ? '0px' : top + height >= clientHeight ? clientHeight - height + 'px' : top + 'px'; - if (clientX <= clientWidth - 400) { - style.left = clientX + 45 + 'px'; - return style.right = null; - } else { - style.left = null; - return style.right = clientWidth - clientX + 45 + 'px'; + el = this.parentNode; + if (isTouching = e.type === 'touchstart') { + e = e.changedTouches[e.changedTouches.length - 1]; } - }, - hoverend: function() { - $.rm(UI.el); - return delete UI.el; - } - }; + rect = el.getBoundingClientRect(); + screenHeight = d.documentElement.clientHeight; + screenWidth = d.documentElement.clientWidth; + o = { + id: el.id, + style: el.style, + dx: e.clientX - rect.left, + dy: e.clientY - rect.top, + height: screenHeight - rect.height, + width: screenWidth - rect.width, + screenHeight: screenHeight, + screenWidth: screenWidth + }; + if (isTouching) { + o.identifier = e.identifier; + o.move = touchmove.bind(o); + o.up = dragend.bind(o); + d.addEventListener('touchmove', o.move, false); + d.addEventListener('touchend', o.up, false); + return d.addEventListener('touchcancel', o.up, false); + } else { + o.move = drag.bind(o); + o.up = dragend.bind(o); + d.addEventListener('mousemove', o.move, false); + return d.addEventListener('mouseup', o.up, false); + } + }; + touchmove = function(e) { + var touch, _i, _len, _ref; + _ref = e.changedTouches; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + touch = _ref[_i]; + if (touch.identifier === this.identifier) { + drag.call(this, touch); + return; + } + } + }; + drag = function(e) { + var left, top; + left = e.clientX - this.dx; + top = e.clientY - this.dy; + if (left < 10) { + left = 0; + } else if (this.width - left < 10) { + left = null; + } + if (top < 10) { + top = 0; + } else if (this.height - top < 10) { + top = null; + } + if (left === null) { + this.style.left = null; + this.style.right = '0%'; + } else { + this.style.left = left / this.screenWidth * 100 + '%'; + this.style.right = null; + } + if (top === null) { + this.style.top = null; + return this.style.bottom = '0%'; + } else { + this.style.top = top / this.screenHeight * 100 + '%'; + return this.style.bottom = null; + } + }; + dragend = function(e) { + if (e.type === 'mouseup') { + d.removeEventListener('mousemove', this.move, false); + d.removeEventListener('mouseup', this.up, false); + } else { + d.removeEventListener('touchmove', this.move, false); + d.removeEventListener('touchend', this.up, false); + d.removeEventListener('touchcancel', this.up, false); + } + return localStorage.setItem("" + g.NAMESPACE + this.id + ".position", this.style.cssText); + }; + hoverstart = function(root, el, events, cb) { + var event, o, _i, _len, _ref; + o = { + root: root, + el: el, + style: el.style, + cb: cb, + events: events.split(' '), + clientHeight: d.documentElement.clientHeight, + clientWidth: d.documentElement.clientWidth + }; + o.hover = hover.bind(o); + o.hoverend = hoverend.bind(o); + _ref = o.events; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + event = _ref[_i]; + root.addEventListener(event, o.hoverend, false); + } + return root.addEventListener('mousemove', o.hover, false); + }; + hover = function(e) { + var clientX, height, top; + height = this.el.offsetHeight; + top = e.clientY - height / 2; + this.style.top = this.clientHeight <= height || top <= 0 ? '0px' : top + height >= this.clientHeight ? this.clientHeight - height + 'px' : top + 'px'; + clientX = e.clientX; + if (clientX <= this.clientWidth - 400) { + this.style.left = clientX + 45 + 'px'; + return this.style.right = null; + } else { + this.style.left = null; + return this.style.right = this.clientWidth - clientX + 45 + 'px'; + } + }; + hoverend = function() { + var event, _i, _len, _ref; + this.el.parentNode.removeChild(this.el); + _ref = this.events; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + event = _ref[_i]; + this.root.removeEventListener(event, this.hoverend, false); + } + this.root.removeEventListener('mousemove', this.hover, false); + if (this.cb) { + return this.cb.call(this); + } + }; + return { + dialog: dialog, + hover: hoverstart + }; + })(); /* loosely follows the jquery api: @@ -1830,20 +1919,15 @@ if ($.hasClass(this, 'inlined')) { return; } - if (UI.el) { - return; - } _ref = Get.postDataFromLink(this), board = _ref.board, threadID = _ref.threadID, postID = _ref.postID; - qp = UI.el = $.el('div', { + qp = $.el('div', { id: 'qp', className: 'reply dialog' }); - UI.hover(e); context = Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', this)); $.add(d.body, qp); Get.postClone(board, threadID, postID, qp, context); - $.on(this, 'mousemove', UI.hover); - $.on(this, 'mouseout click', QuotePreview.mouseout); + UI.hover(this, qp, 'mouseout click', QuotePreview.mouseout); if (!(origin = g.posts["" + board + "." + postID])) { return; } @@ -1872,12 +1956,9 @@ } } }, - mouseout: function(e) { + mouseout: function() { var clone, post, root, _i, _len, _ref; - root = UI.el.firstElementChild; - UI.hoverend(); - $.off(this, 'mousemove', UI.hover); - $.off(this, 'mouseout click', QuotePreview.mouseout); + root = this.el.firstElementChild; if (!root) { return; } @@ -2357,30 +2438,29 @@ return $.on(this.file.thumb, 'mouseover', ImageHover.mouseover); }, mouseover: function() { - var el; - if (UI.el) { - return; - } - el = UI.el = $.el('img', { + var el, + _this = this; + el = $.el('img', { id: 'ihover', src: this.parentNode.href }); $.add(d.body, el); - $.on(el, 'load', ImageHover.load); + $.on(el, 'load', function() { + return ImageHover.load(_this, el); + }); $.on(el, 'error', ImageHover.error); - $.on(this, 'mousemove', UI.hover); - return $.on(this, 'mouseout', ImageHover.mouseout); + return UI.hover(this, el, 'mouseout'); }, - load: function() { - var style; - if (!this.parentNode) { + load: function(root, el) { + var e, style; + if (!el.parentNode) { return; } - style = this.style; - return UI.hover({ - clientX: -45 + parseInt(style.left), - clientY: 120 + parseInt(style.top) - }); + style = el.style; + e = new Event('mousemove'); + e.clientX = -45 + parseInt(style.left); + e.clientY = 120 + parseInt(style.top); + return root.dispatchEvent(e); }, error: function() { var src, timeoutID, url, @@ -2413,11 +2493,6 @@ }, { type: 'head' }); - }, - mouseout: function() { - UI.hoverend(); - $.off(this, 'mousemove', UI.hover); - return $.off(this, 'mouseout', ImageHover.mouseout); } }; diff --git a/changelog b/changelog index 8e9f27e6d..e812ffd98 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,6 @@ alpha - Mayhem + Added touch and multi-touch support for dragging windows. The Thread Updater will pause when offline, and resume when online. Fix Chrome's install warning that 4chan X would execute on all domains. Fix Quote Backlinks not affecting inlined quotes. diff --git a/script.coffee b/script.coffee index 618905376..d3f9aec80 100644 --- a/script.coffee +++ b/script.coffee @@ -175,72 +175,129 @@ g = threads: {} posts: {} -UI = - dialog: (id, position, html) -> +UI = (-> + dialog = (id, position, html) -> el = d.createElement 'div' el.className = 'reply dialog' el.innerHTML = html el.id = id el.style.cssText = localStorage.getItem("#{g.NAMESPACE}#{id}.position") or position - el.querySelector('.move').addEventListener 'mousedown', UI.dragstart, false + move = el.querySelector '.move' + move.addEventListener 'touchstart', dragstart, false + move.addEventListener 'mousedown', dragstart, false el - dragstart: (e) -> + + dragstart = (e) -> # prevent text selection e.preventDefault() - UI.el = el = @parentNode - d.addEventListener 'mousemove', UI.drag, false - d.addEventListener 'mouseup', UI.dragend, false + el = @parentNode + if isTouching = e.type is 'touchstart' + e = e.changedTouches[e.changedTouches.length - 1] # distance from pointer to el edge is constant; calculate it here. - rect = el.getBoundingClientRect() - UI.dx = e.clientX - rect.left - UI.dy = e.clientY - rect.top - UI.width = d.documentElement.clientWidth - rect.width - UI.height = d.documentElement.clientHeight - rect.height - drag: (e) -> - left = e.clientX - UI.dx - top = e.clientY - UI.dy - left = - if left < 10 then '0px' - else if UI.width - left < 10 then null - else left + 'px' - top = - if top < 10 then '0px' - else if UI.height - top < 10 then null - else top + 'px' - {style} = UI.el - style.left = left - style.top = top - style.right = if left then null else '0px' - style.bottom = if top then null else '0px' - dragend: -> - localStorage.setItem "#{g.NAMESPACE}#{UI.el.id}.position", UI.el.style.cssText - d.removeEventListener 'mousemove', UI.drag, false - d.removeEventListener 'mouseup', UI.dragend, false - delete UI.el - hover: (e) -> - {clientX, clientY} = e - {style} = UI.el - {clientHeight, clientWidth} = d.documentElement - height = UI.el.offsetHeight + rect = el.getBoundingClientRect() + screenHeight = d.documentElement.clientHeight + screenWidth = d.documentElement.clientWidth + o = { + id: el.id + style: el.style + dx: e.clientX - rect.left + dy: e.clientY - rect.top + height: screenHeight - rect.height + width: screenWidth - rect.width + screenHeight: screenHeight + screenWidth: screenWidth + } + if isTouching + o.identifier = e.identifier + o.move = touchmove.bind o + o.up = dragend.bind o + d.addEventListener 'touchmove', o.move, false + d.addEventListener 'touchend', o.up, false + d.addEventListener 'touchcancel', o.up, false + else # mousedown + o.move = drag.bind o + o.up = dragend.bind o + d.addEventListener 'mousemove', o.move, false + d.addEventListener 'mouseup', o.up, false + touchmove = (e) -> + for touch in e.changedTouches + if touch.identifier is @identifier + drag.call @, touch + return + drag = (e) -> + left = e.clientX - @dx + top = e.clientY - @dy + if left < 10 then left = 0 + else if @width - left < 10 then left = null + if top < 10 then top = 0 + else if @height - top < 10 then top = null + if left is null + @style.left = null + @style.right = '0%' + else + @style.left = left / @screenWidth * 100 + '%' + @style.right = null + if top is null + @style.top = null + @style.bottom = '0%' + else + @style.top = top / @screenHeight * 100 + '%' + @style.bottom = null + dragend = (e) -> + if e.type is 'mouseup' + d.removeEventListener 'mousemove', @move, false + d.removeEventListener 'mouseup', @up, false + else # touchend or touchcancel + d.removeEventListener 'touchmove', @move, false + d.removeEventListener 'touchend', @up, false + d.removeEventListener 'touchcancel', @up, false + localStorage.setItem "#{g.NAMESPACE}#{@id}.position", @style.cssText - top = clientY - 120 - style.top = - if clientHeight <= height or top <= 0 + hoverstart = (root, el, events, cb) -> + o = { + root: root + el: el + style: el.style + cb: cb + events: events.split ' ' + clientHeight: d.documentElement.clientHeight + clientWidth: d.documentElement.clientWidth + } + o.hover = hover.bind o + o.hoverend = hoverend.bind o + for event in o.events + root.addEventListener event, o.hoverend, false + root.addEventListener 'mousemove', o.hover, false + hover = (e) -> + height = @el.offsetHeight + top = e.clientY - height / 2 + @style.top = + if @clientHeight <= height or top <= 0 '0px' - else if top + height >= clientHeight - clientHeight - height + 'px' + else if top + height >= @clientHeight + @clientHeight - height + 'px' else top + 'px' - if clientX <= clientWidth - 400 - style.left = clientX + 45 + 'px' - style.right = null + {clientX} = e + if clientX <= @clientWidth - 400 + @style.left = clientX + 45 + 'px' + @style.right = null else - style.left = null - style.right = clientWidth - clientX + 45 + 'px' - hoverend: -> - $.rm UI.el - delete UI.el + @style.left = null + @style.right = @clientWidth - clientX + 45 + 'px' + hoverend = -> + @el.parentNode.removeChild @el + for event in @events + @root.removeEventListener event, @hoverend, false + @root.removeEventListener 'mousemove', @hover, false + @cb.call @ if @cb + + return { + dialog: dialog + hover: hoverstart + } +)() ### loosely follows the jquery api: @@ -1790,21 +1847,16 @@ QuotePreview = mouseover: (e) -> return if $.hasClass @, 'inlined' - # Don't stop other elements from dragging - return if UI.el - {board, threadID, postID} = Get.postDataFromLink @ - qp = UI.el = $.el 'div', + qp = $.el 'div', id: 'qp' className: 'reply dialog' - UI.hover e context = Get.postFromRoot $.x 'ancestor::div[parent::div[@class="thread"]][1]', @ $.add d.body, qp Get.postClone board, threadID, postID, qp, context - $.on @, 'mousemove', UI.hover - $.on @, 'mouseout click', QuotePreview.mouseout + UI.hover @, qp, 'mouseout click', QuotePreview.mouseout return unless origin = g.posts["#{board}.#{postID}"] @@ -1824,11 +1876,8 @@ QuotePreview = if quote.hash[2..] is quoterID $.addClass quote, 'forwardlink' return - mouseout: (e) -> - root = UI.el.firstElementChild - UI.hoverend() - $.off @, 'mousemove', UI.hover - $.off @, 'mouseout click', QuotePreview.mouseout + mouseout: -> + root = @el.firstElementChild # Stop if it only contains text. return unless root @@ -2141,23 +2190,21 @@ ImageHover = return unless @file?.isImage $.on @file.thumb, 'mouseover', ImageHover.mouseover mouseover: -> - # Don't stop other elements from dragging - return if UI.el - el = UI.el = $.el 'img' + el = $.el 'img' id: 'ihover' src: @parentNode.href $.add d.body, el - $.on el, 'load', ImageHover.load - $.on el, 'error', ImageHover.error - $.on @, 'mousemove', UI.hover - $.on @, 'mouseout', ImageHover.mouseout - load: -> - return unless @parentNode + $.on el, 'load', => ImageHover.load @, el + $.on el, 'error', ImageHover.error + UI.hover @, el, 'mouseout' + load: (root, el) -> + return unless el.parentNode # 'Fake' mousemove event by giving required values. - {style} = @ - UI.hover - clientX: - 45 + parseInt style.left - clientY: 120 + parseInt style.top + {style} = el + e = new Event 'mousemove' + e.clientX = - 45 + parseInt style.left + e.clientY = 120 + parseInt style.top + root.dispatchEvent e error: -> return unless @parentNode src = @src.split '/' @@ -2171,10 +2218,6 @@ ImageHover = return if $.engine isnt 'webkit' or url.split('/')[2] isnt 'images.4chan.org' $.ajax url, onreadystatechange: (-> clearTimeout timeoutID if @status is 404), type: 'head' - mouseout: -> - UI.hoverend() - $.off @, 'mousemove', UI.hover - $.off @, 'mouseout', ImageHover.mouseout ThreadUpdater = init: ->