From 0210c4130bc71e99a63804e04dc03b115e5e06fa Mon Sep 17 00:00:00 2001 From: Dima Voytenko Date: Fri, 25 Sep 2015 22:28:05 -0700 Subject: [PATCH] DOM and other helper additions needed for Lightbox --- src/dom.js | 86 +++++++++++++++++++++++++ src/event-helper.js | 12 +++- src/layout-rect.js | 18 ++++++ test/functional/test-dom.js | 96 ++++++++++++++++++++++++++++ test/functional/test-event-helper.js | 15 ++++- test/functional/test-layout-rect.js | 10 +++ 6 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 src/dom.js create mode 100644 test/functional/test-dom.js diff --git a/src/dom.js b/src/dom.js new file mode 100644 index 000000000000..84ebee075394 --- /dev/null +++ b/src/dom.js @@ -0,0 +1,86 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * Removes all child nodes of the specified element. + * @param {!Element} parent + */ +export function removeChildren(parent) { + while (parent.firstChild) { + parent.removeChild(parent.firstChild); + } +} + + +/** + * Copies all children nodes of element "from" to element "to". Child nodes + * are deeply cloned. Notice, that this method should be used with care and + * preferrably on smaller subtrees. + * @param {!Element} from + * @param {!Element} to + */ +export function copyChildren(from, to) { + let frag = to.ownerDocument.createDocumentFragment(); + for (let n = from.firstChild; n; n = n.nextSibling) { + frag.appendChild(n.cloneNode(true)); + } + to.appendChild(frag); +} + + +/** + * Finds the closest element that satisfies the callback from this element + * up the DOM subtree. + * @param {!Element} element + * @param {function(!Element):boolean} callback + * @return {?Element} + */ +export function closest(element, callback) { + for (let el = element; el; el = el.parentElement) { + if (callback(el)) { + return el; + } + } + return null; +} + + +/** + * Finds the closest element with the specified name from this element + * up the DOM subtree. + * @param {!Element} element + * @param {string} tagName + * @return {?Element} + */ +export function closestByTag(element, tagName) { + tagName = tagName.toUpperCase(); + return closest(element, (el) => { + return el.tagName == tagName; + }); +} + + +/** + * Finds the first descendant element with the specified name. + * @param {!Element} element + * @param {string} tagName + * @return {?Element} + */ +export function elementByTag(element, tagName) { + let elements = element.getElementsByTagName(tagName); + return elements.length > 0 ? elements[0] : null; +} diff --git a/src/event-helper.js b/src/event-helper.js index 151fa6944807..13fb7dfdfffd 100644 --- a/src/event-helper.js +++ b/src/event-helper.js @@ -61,6 +61,16 @@ export function listenOncePromise(element, eventType, opt_capture, } +/** + * Whether the specified element has been loaded already. + * @param {!Element} element + * @return {boolean} + */ +export function isLoaded(element) { + return element.complete || element.readyState == 'complete'; +} + + /** * Returns a promise that will resolve or fail based on the element's 'load' * and 'error' events. Optionally this method takes a timeout, which will reject @@ -73,7 +83,7 @@ export function loadPromise(element, opt_timeout) { let unlistenLoad; let unlistenError; let loadingPromise = new Promise((resolve, reject) => { - if (element.complete || element.readyState == 'complete') { + if (isLoaded(element)) { resolve(element); } else { // Listen once since IE 5/6/7 fire the onload event continuously for diff --git a/src/layout-rect.js b/src/layout-rect.js index 6d94fb1189e8..d93c6f1cc259 100644 --- a/src/layout-rect.js +++ b/src/layout-rect.js @@ -52,6 +52,24 @@ export function layoutRectLtwh(left, top, width, height) { } +/** + * Creates a layout rect based on the DOMRect, e.g. obtained from calling + * getBoundingClientRect. + * @param {!DOMRect} rect + * @return {!LayoutRect} + */ +export function layoutRectFromDomRect(rect) { + return { + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + bottom: rect.top + rect.height, + right: rect.left + rect.width + }; +} + + /** * Returns true if the specified two rects overlap by a single pixel. * @param {!LayoutRect} r1 diff --git a/test/functional/test-dom.js b/test/functional/test-dom.js new file mode 100644 index 000000000000..6bc008ab95e7 --- /dev/null +++ b/test/functional/test-dom.js @@ -0,0 +1,96 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as dom from '../../src/dom'; + + +describe('DOM', () => { + + it('should remove all children', () => { + let element = document.createElement('div'); + element.appendChild(document.createElement('div')); + element.appendChild(document.createTextNode('ABC')); + expect(element.children.length).to.equal(1); + expect(element.firstChild).to.not.equal(null); + expect(element.textContent).to.equal('ABC'); + + dom.removeChildren(element); + expect(element.children.length).to.equal(0); + expect(element.firstChild).to.equal(null); + expect(element.textContent).to.equal(''); + }); + + it('should copy all children', () => { + let element = document.createElement('div'); + element.appendChild(document.createElement('div')); + element.appendChild(document.createTextNode('ABC')); + + let other = document.createElement('div'); + dom.copyChildren(element, other); + + expect(element.children.length).to.equal(1); + expect(element.firstChild).to.not.equal(null); + expect(element.textContent).to.equal('ABC'); + + expect(other.children.length).to.equal(1); + expect(other.firstChild).to.not.equal(null); + expect(other.firstChild.tagName).to.equal('DIV'); + expect(other.textContent).to.equal('ABC'); + }); + + it('closest should find itself', () => { + let element = document.createElement('div'); + + let child = document.createElement('div'); + element.appendChild(child); + + expect(dom.closest(child, () => true)).to.equal(child); + expect(dom.closestByTag(child, 'div')).to.equal(child); + expect(dom.closestByTag(child, 'DIV')).to.equal(child); + }); + + it('closest should find first match', () => { + let parent = document.createElement('parent'); + + let element = document.createElement('element'); + parent.appendChild(element); + + let child = document.createElement('child'); + element.appendChild(child); + + expect(dom.closest(child, (e) => e.tagName == 'CHILD')).to.equal(child); + expect(dom.closestByTag(child, 'child')).to.equal(child); + + expect(dom.closest(child, (e) => e.tagName == 'ELEMENT')).to.equal(element); + expect(dom.closestByTag(child, 'element')).to.equal(element); + + expect(dom.closest(child, (e) => e.tagName == 'PARENT')).to.equal(parent); + expect(dom.closestByTag(child, 'parent')).to.equal(parent); + }); + + it('closest should find first match', () => { + let parent = document.createElement('parent'); + + let element1 = document.createElement('element'); + parent.appendChild(element1); + + let element2 = document.createElement('element'); + parent.appendChild(element2); + + expect(dom.elementByTag(parent, 'element')).to.equal(element1); + expect(dom.elementByTag(parent, 'ELEMENT')).to.equal(element1); + }); +}); diff --git a/test/functional/test-event-helper.js b/test/functional/test-event-helper.js index c17fe6a3727d..98a6ca6d7b0f 100644 --- a/test/functional/test-event-helper.js +++ b/test/functional/test-event-helper.js @@ -14,7 +14,8 @@ * limitations under the License. */ -import {listenOnce, listenOncePromise, loadPromise} from '../../src/event-helper'; +import {isLoaded, listenOnce, listenOncePromise, loadPromise} + from '../../src/event-helper'; import {Observable} from '../../src/observable'; describe('EventHelper', () => { @@ -120,6 +121,18 @@ describe('EventHelper', () => { return promise; }); + it('isLoaded for complete property', () => { + expect(isLoaded(element)).to.equal(false); + element.complete = true; + expect(isLoaded(element)).to.equal(true); + }); + + it('isLoaded for readyState property', () => { + expect(isLoaded(element)).to.equal(false); + element.readyState = 'complete'; + expect(isLoaded(element)).to.equal(true); + }); + it('loadPromise - already complete', () => { element.complete = true; return loadPromise(element).then((result) => { diff --git a/test/functional/test-layout-rect.js b/test/functional/test-layout-rect.js index b97341d6594e..f823cd1e114d 100644 --- a/test/functional/test-layout-rect.js +++ b/test/functional/test-layout-rect.js @@ -48,4 +48,14 @@ describe('LayoutRect', () => { expect(rect2.height).to.equal(40 + 40 * 6); }); + it('layoutRectFromDomRect', () => { + let rect = lr.layoutRectFromDomRect({top: 11, left: 12, width: 111, + height: 222}); + expect(rect.top).to.equal(11); + expect(rect.left).to.equal(12); + expect(rect.width).to.equal(111); + expect(rect.height).to.equal(222); + expect(rect.bottom).to.equal(11 + 222); + expect(rect.right).to.equal(12 + 111); + }); });