Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
848 lines
25 KiB
JavaScript
Executable file
848 lines
25 KiB
JavaScript
Executable file
'use strict';
|
|
|
|
window.GaiaContainer = (function(exports) {
|
|
/**
|
|
* The time, in ms, to wait for an animation to start in response to a state
|
|
* change, before removing the associated style class.
|
|
*/
|
|
const STATE_CHANGE_TIMEOUT = 100;
|
|
|
|
/**
|
|
* The time, in ms, to wait before initiating a drag-and-drop from a
|
|
* long-press.
|
|
*/
|
|
const DEFAULT_DND_TIMEOUT = 300;
|
|
|
|
/**
|
|
* The distance, in CSS pixels, in which a touch or mouse point can deviate
|
|
* before it being discarded for initiation of a drag-and-drop action.
|
|
*/
|
|
const DND_THRESHOLD = 5;
|
|
|
|
/**
|
|
* The minimum time between sending move events during drag-and-drop.
|
|
*/
|
|
const DND_MOVE_THROTTLE = 50;
|
|
|
|
var proto = Object.create(HTMLElement.prototype);
|
|
|
|
proto.createdCallback = function() {
|
|
this._template = template.content.cloneNode(true);
|
|
|
|
var shadow = this.createShadowRoot();
|
|
shadow.appendChild(this._template);
|
|
|
|
this._frozen = false;
|
|
this._pendingStateChanges = [];
|
|
this._children = [];
|
|
this._dnd = {
|
|
// Whether drag-and-drop is enabled
|
|
enabled: false,
|
|
|
|
// The time, in ms, to wait before initiating a drag-and-drop from a
|
|
// long-press
|
|
delay: DEFAULT_DND_TIMEOUT,
|
|
|
|
// Timeout used to initiate drag-and-drop actions
|
|
timeout: null,
|
|
|
|
// The child that was tapped/clicked
|
|
child: null,
|
|
|
|
// Whether a drag is active
|
|
active: false,
|
|
|
|
// The start point of the drag action
|
|
start: { pageX: 0, pageY: 0, clientX: 0, clientY: 0 },
|
|
|
|
// The last point of the drag action
|
|
last: { pageX: 0, pageY: 0, clientX: 0, clientY: 0, timeStamp: 0 },
|
|
|
|
// Timeout used to send drag-move events
|
|
moveTimeout: null,
|
|
|
|
// The last time a move event was fired
|
|
lastMoveEventTime: 0,
|
|
|
|
// Whether to capture the next click event
|
|
clickCapture: false
|
|
};
|
|
|
|
var dndObserverCallback = (mutations) => {
|
|
if (this._dnd.enabled !== this.dragAndDrop) {
|
|
this._dnd.enabled = this.dragAndDrop;
|
|
if (this._dnd.enabled) {
|
|
this.addEventListener('touchstart', this);
|
|
this.addEventListener('touchmove', this);
|
|
this.addEventListener('touchcancel', this);
|
|
this.addEventListener('touchend', this);
|
|
this.addEventListener('mousedown', this);
|
|
this.addEventListener('mousemove', this);
|
|
this.addEventListener('mouseup', this);
|
|
this.addEventListener('click', this, true);
|
|
this.addEventListener('contextmenu', this, true);
|
|
} else {
|
|
this.cancelDrag();
|
|
this.removeEventListener('touchstart', this);
|
|
this.removeEventListener('touchmove', this);
|
|
this.removeEventListener('touchcancel', this);
|
|
this.removeEventListener('touchend', this);
|
|
this.removeEventListener('mousedown', this);
|
|
this.removeEventListener('mousemove', this);
|
|
this.removeEventListener('mouseup', this);
|
|
this.removeEventListener('click', this, true);
|
|
this.removeEventListener('contextmenu', this, true);
|
|
}
|
|
}
|
|
};
|
|
this.dndObserver = new MutationObserver(dndObserverCallback);
|
|
this.dndObserver.observe(this,
|
|
{ attributes: true, attributeFilter: ['drag-and-drop'] });
|
|
dndObserverCallback(null);
|
|
};
|
|
|
|
Object.defineProperty(proto, 'children', {
|
|
get: function() {
|
|
return this._children.map((child) => {
|
|
return child.element;
|
|
});
|
|
},
|
|
enumerable: true
|
|
});
|
|
|
|
Object.defineProperty(proto, 'firstChild', {
|
|
get: function() {
|
|
return this._children.length ? this._children[0].element : null;
|
|
},
|
|
enumerable: true
|
|
});
|
|
|
|
Object.defineProperty(proto, 'lastChild', {
|
|
get: function() {
|
|
var length = this._children.length;
|
|
return length ? this._children[length - 1].element : null;
|
|
},
|
|
enumerable: true
|
|
});
|
|
|
|
Object.defineProperty(proto, 'dragAndDrop', {
|
|
get: function() {
|
|
return this.getAttribute('drag-and-drop') !== null;
|
|
},
|
|
enumerable: true
|
|
});
|
|
|
|
Object.defineProperty(proto, 'dragAndDropTimeout', {
|
|
get: function() {
|
|
return this._dnd.delay;
|
|
},
|
|
set: function(timeout) {
|
|
if (timeout >= 0) {
|
|
this._dnd.delay = timeout;
|
|
} else {
|
|
this._dnd.delay = DEFAULT_DND_TIMEOUT;
|
|
}
|
|
},
|
|
enumerable: true
|
|
});
|
|
|
|
proto.realAppendChild = proto.appendChild;
|
|
proto.appendChild = function(element, callback) {
|
|
this.insertBefore(element, null, callback);
|
|
};
|
|
|
|
proto.realRemoveChild = proto.removeChild;
|
|
proto.removeChild = function(element, callback) {
|
|
var children = this._children;
|
|
var childToRemove = null;
|
|
|
|
for (var child of children) {
|
|
if (child.element === element) {
|
|
childToRemove = child;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (childToRemove === null) {
|
|
throw 'removeChild called on unknown child';
|
|
}
|
|
|
|
this.changeState(childToRemove, 'removed', () => {
|
|
this.realRemoveChild(childToRemove.container);
|
|
this.realRemoveChild(childToRemove.master);
|
|
|
|
// Find the child again. We need to do this in case the container was
|
|
// manipulated between removing the child and this callback being reached.
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
if (children[i] === childToRemove) {
|
|
children.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
|
|
this.synchronise();
|
|
});
|
|
};
|
|
|
|
proto.realReplaceChild = proto.replaceChild;
|
|
proto.replaceChild = function(newElement, oldElement, callback) {
|
|
if (!newElement || !oldElement) {
|
|
throw 'replaceChild called with null arguments';
|
|
}
|
|
|
|
// Unparent the newElement if necessary (with support for gaia-container)
|
|
if (newElement.parentNode) {
|
|
newElement.parentNode.removeChild(newElement, () => {
|
|
this.replaceChild(newElement, oldElement, callback);
|
|
});
|
|
if (newElement.parentNode) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Remove the old child and add the new one, but don't run the removed/
|
|
// added state changes.
|
|
var children = this._children;
|
|
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
var oldChild = children[i];
|
|
if (oldChild.element === oldElement) {
|
|
var newChild = new GaiaContainerChild(newElement);
|
|
this.realInsertBefore(newChild.container, oldChild.container);
|
|
this.realInsertBefore(newChild.master, oldChild.master);
|
|
this.realRemoveChild(oldChild.container);
|
|
this.realRemoveChild(oldChild.master);
|
|
this.children.splice(i, 1, newChild);
|
|
this.synchronise();
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw 'removeChild called on unknown child';
|
|
};
|
|
|
|
/**
|
|
* Reorders the given element to appear before referenceElement.
|
|
*/
|
|
proto.reorderChild = function(element, referenceElement, callback) {
|
|
if (!element) {
|
|
throw 'reorderChild called with null element';
|
|
}
|
|
|
|
var children = this._children;
|
|
var child = null;
|
|
var childIndex = null;
|
|
var referenceChild = null;
|
|
var referenceChildIndex = null;
|
|
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
if (children[i].element === element) {
|
|
child = children[i];
|
|
childIndex = i;
|
|
} else if (children[i].element === referenceElement) {
|
|
referenceChild = children[i];
|
|
referenceChildIndex = i;
|
|
}
|
|
|
|
if (child && (referenceChild || !referenceElement)) {
|
|
this.realRemoveChild(child.container);
|
|
this.realRemoveChild(child.master);
|
|
children.splice(childIndex, 1);
|
|
|
|
if (referenceChild) {
|
|
this.realInsertBefore(child.container, referenceChild.container);
|
|
this.realInsertBefore(child.master, referenceChild.master);
|
|
} else {
|
|
(children.length === 0) ?
|
|
this.realAppendChild(child.container) :
|
|
this.realInsertBefore(child.container, children[0].master);
|
|
this.realAppendChild(child.master);
|
|
}
|
|
|
|
referenceChild ?
|
|
children.splice(
|
|
referenceChildIndex - (childIndex < referenceChildIndex) ? 1 : 0,
|
|
0, child) :
|
|
children.splice(children.length, 0, child);
|
|
|
|
this.synchronise();
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw child ? 'reorderChild called on unknown reference element' :
|
|
'reorderChild called on unknown child';
|
|
};
|
|
|
|
proto.realInsertBefore = proto.insertBefore;
|
|
proto.insertBefore = function(element, reference, callback) {
|
|
var children = this._children;
|
|
var childToInsert = new GaiaContainerChild(element);
|
|
var referenceIndex = -1;
|
|
|
|
if (reference !== null) {
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
if (children[i].element === reference) {
|
|
referenceIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (referenceIndex === -1) {
|
|
throw 'insertBefore called on unknown child';
|
|
}
|
|
}
|
|
|
|
if (referenceIndex === -1) {
|
|
(children.length === 0) ?
|
|
this.realAppendChild(childToInsert.container) :
|
|
this.realInsertBefore(childToInsert.container, children[0].master);
|
|
this.realAppendChild(childToInsert.master);
|
|
children.push(childToInsert);
|
|
} else {
|
|
this.realInsertBefore(childToInsert.container,
|
|
children[referenceIndex].container);
|
|
this.realInsertBefore(childToInsert.master,
|
|
children[referenceIndex].master);
|
|
children.splice(referenceIndex, 0, childToInsert);
|
|
}
|
|
|
|
this.changeState(childToInsert, 'added', callback);
|
|
this.synchronise();
|
|
};
|
|
|
|
/**
|
|
* Used to execute a state-change of a child that may possibly be animated.
|
|
* @state will be added to the child's class-list. If an animation starts that
|
|
* has the same name, that animation will complete and @callback will be
|
|
* called. Otherwise, the class will be removed and @callback called on the
|
|
* next frame.
|
|
*/
|
|
proto.changeState = function(child, state, callback) {
|
|
// Check that the child is still attached to this parent (can happen if
|
|
// the child is removed while frozen).
|
|
if (child.container.parentNode !== this) {
|
|
return;
|
|
}
|
|
|
|
// Check for a redundant state change.
|
|
if (child.container.classList.contains(state)) {
|
|
return;
|
|
}
|
|
|
|
// Queue up state change if we're frozen.
|
|
if (this._frozen) {
|
|
this._pendingStateChanges.push(
|
|
this.changeState.bind(this, child, state, callback));
|
|
return;
|
|
}
|
|
|
|
var animStart = (e) => {
|
|
if (!e.animationName.endsWith(state)) {
|
|
return;
|
|
}
|
|
|
|
child.container.removeEventListener('animationstart', animStart);
|
|
|
|
window.clearTimeout(child[state]);
|
|
delete child[state];
|
|
|
|
var self = this;
|
|
child.container.addEventListener('animationend', function animEnd() {
|
|
child.container.removeEventListener('animationend', animEnd);
|
|
child.container.classList.remove(state);
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
};
|
|
|
|
child.container.addEventListener('animationstart', animStart);
|
|
child.container.classList.add(state);
|
|
|
|
child[state] = window.setTimeout(() => {
|
|
delete child[state];
|
|
child.container.removeEventListener('animationstart', animStart);
|
|
child.container.classList.remove(state);
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}, STATE_CHANGE_TIMEOUT);
|
|
};
|
|
|
|
proto.getChildOffsetRect = function(element) {
|
|
var children = this._children;
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
var child = children[i];
|
|
if (child.element === element) {
|
|
var top = child._lastMasterTop;
|
|
var left = child._lastMasterLeft;
|
|
var width = child._lastElementWidth;
|
|
var height = child._lastElementHeight;
|
|
|
|
return {
|
|
top: top,
|
|
left: left,
|
|
width: width,
|
|
height: height,
|
|
right: left + width,
|
|
bottom: top + height
|
|
};
|
|
}
|
|
}
|
|
|
|
throw 'getChildOffsetRect called on unknown child';
|
|
};
|
|
|
|
proto.getChildFromPoint = function(x, y) {
|
|
var children = this._children;
|
|
for (var parent = this.parentElement; parent;
|
|
parent = parent.parentElement) {
|
|
x += parent.scrollLeft - parent.offsetLeft;
|
|
y += parent.scrollTop - parent.offsetTop;
|
|
}
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
var child = children[i];
|
|
if (x >= child._lastMasterLeft &&
|
|
y >= child._lastMasterTop &&
|
|
x < child._lastMasterLeft + child._lastElementWidth &&
|
|
y < child._lastMasterTop + child._lastElementHeight) {
|
|
return child.element;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
proto.cancelDrag = function() {
|
|
if (this._dnd.timeout !== null) {
|
|
clearTimeout(this._dnd.timeout);
|
|
this._dnd.timeout = null;
|
|
}
|
|
|
|
if (this._dnd.moveTimeout !== null) {
|
|
clearTimeout(this._dnd.moveTimeout);
|
|
this._dnd.moveTimeout = null;
|
|
}
|
|
|
|
if (this._dnd.active) {
|
|
this._dnd.child.container.classList.remove('dragging');
|
|
this._dnd.child.container.style.position = 'absolute';
|
|
this._dnd.child.container.style.top = '0';
|
|
this._dnd.child.container.style.left = '0';
|
|
this._dnd.child.markDirty();
|
|
this._dnd.child = null;
|
|
this._dnd.active = false;
|
|
this.synchronise();
|
|
this._dnd.clickCapture = true;
|
|
this.dispatchEvent(new CustomEvent('drag-finish'));
|
|
}
|
|
};
|
|
|
|
proto.startDrag = function() {
|
|
if (!this.dispatchEvent(new CustomEvent('drag-start',
|
|
{ cancelable: true,
|
|
detail: { target: this._dnd.child.element,
|
|
pageX: this._dnd.start.pageX,
|
|
pageY: this._dnd.start.pageY,
|
|
clientX: this._dnd.start.clientX,
|
|
clientY: this._dnd.start.clientY } }))) {
|
|
return;
|
|
}
|
|
|
|
this._dnd.active = true;
|
|
this._dnd.child.container.classList.add('dragging');
|
|
this._dnd.child.container.style.position = 'fixed';
|
|
var rect = this.getBoundingClientRect();
|
|
this._dnd.child.container.style.top = rect.top + 'px';
|
|
this._dnd.child.container.style.left = rect.left + 'px';
|
|
};
|
|
|
|
proto.continueDrag = function() {
|
|
if (!this._dnd.active) {
|
|
return;
|
|
}
|
|
|
|
var left = this._dnd.child.master.offsetLeft +
|
|
(this._dnd.last.pageX - this._dnd.start.pageX);
|
|
var top = this._dnd.child.master.offsetTop +
|
|
(this._dnd.last.pageY - this._dnd.start.pageY);
|
|
this._dnd.child.container.style.transform =
|
|
'translate(' + left + 'px, ' + top + 'px)';
|
|
|
|
if (this._dnd.moveTimeout === null) {
|
|
var delay = Math.max(0, DND_MOVE_THROTTLE -
|
|
(this._dnd.last.timeStamp - this._dnd.lastMoveEventTime));
|
|
this._dnd.moveTimeout = setTimeout(() => {
|
|
this._dnd.moveTimeout = null;
|
|
this._dnd.lastMoveEventTime = this._dnd.last.timeStamp;
|
|
this.dispatchEvent(new CustomEvent('drag-move',
|
|
{ detail: { target: this._dnd.child.element,
|
|
pageX: this._dnd.last.pageX,
|
|
pageY: this._dnd.last.pageY,
|
|
clientX: this._dnd.last.clientX,
|
|
clientY: this._dnd.last.clientY } }));
|
|
}, delay);
|
|
}
|
|
};
|
|
|
|
proto.endDrag = function(event) {
|
|
if (this._dnd.active) {
|
|
var dropTarget = this.getChildFromPoint(this._dnd.last.clientX,
|
|
this._dnd.last.clientY);
|
|
|
|
if (this.dispatchEvent(new CustomEvent('drag-end',
|
|
{ cancelable: true,
|
|
detail: { target: this._dnd.child.element,
|
|
dropTarget: dropTarget,
|
|
pageX: this._dnd.last.pageX,
|
|
pageY: this._dnd.last.pageY,
|
|
clientX: this._dnd.last.clientX,
|
|
clientY: this._dnd.last.clientY }}))) {
|
|
var children = this._children;
|
|
|
|
if (dropTarget && dropTarget !== this._dnd.child.element) {
|
|
var dropChild = null;
|
|
var dropIndex = -1;
|
|
var childIndex = -1;
|
|
var insertBefore = true;
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
if (children[i] === this._dnd.child) {
|
|
childIndex = i;
|
|
if (!dropChild) {
|
|
insertBefore = false;
|
|
}
|
|
}
|
|
|
|
if (children[i].element === dropTarget) {
|
|
dropChild = children[i];
|
|
dropIndex = i;
|
|
}
|
|
|
|
if (dropIndex >= 0 && childIndex >= 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (dropIndex >= 0 && childIndex >= 0) {
|
|
// Default action, rearrange the dragged child to before or after
|
|
// the child underneath the touch/mouse point.
|
|
this.realRemoveChild(this._dnd.child.container);
|
|
this.realRemoveChild(this._dnd.child.master);
|
|
this.realInsertBefore(this._dnd.child.container,
|
|
insertBefore ? dropChild.container :
|
|
dropChild.container.nextSibling);
|
|
this.realInsertBefore(this._dnd.child.master,
|
|
insertBefore ? dropChild.master :
|
|
dropChild.master.nextSibling);
|
|
children.splice(dropIndex, 0,
|
|
children.splice(childIndex, 1)[0]);
|
|
this.dispatchEvent(new CustomEvent('drag-rearrange'));
|
|
}
|
|
}
|
|
}
|
|
} else if (this._dnd.timeout !== null) {
|
|
var handled = !this.dispatchEvent(new CustomEvent('activate',
|
|
{ cancelable : true, detail: { target: this._dnd.child.element } }));
|
|
if (handled) {
|
|
event.stopImmediatePropagation();
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
this.cancelDrag();
|
|
};
|
|
|
|
proto.handleEvent = function(event) {
|
|
switch (event.type) {
|
|
case 'touchstart':
|
|
case 'mousedown':
|
|
if (this._dnd.active || this._dnd.timeout) {
|
|
this.cancelDrag();
|
|
break;
|
|
}
|
|
|
|
if (event instanceof MouseEvent) {
|
|
this._dnd.start.pageX = event.pageX;
|
|
this._dnd.start.pageY = event.pageY;
|
|
this._dnd.start.clientX = event.pageX;
|
|
this._dnd.start.clientY = event.pageY;
|
|
} else {
|
|
this._dnd.start.pageX = event.touches[0].pageX;
|
|
this._dnd.start.pageY = event.touches[0].pageY;
|
|
this._dnd.start.clientX = event.touches[0].clientX;
|
|
this._dnd.start.clientY = event.touches[0].clientY;
|
|
}
|
|
|
|
this._dnd.last.pageX = this._dnd.start.pageX;
|
|
this._dnd.last.pageY = this._dnd.start.pageY;
|
|
this._dnd.last.clientX = this._dnd.start.clientX;
|
|
this._dnd.last.clientY = this._dnd.start.clientY;
|
|
this._dnd.last.timeStamp = event.timeStamp;
|
|
|
|
var target = event.target;
|
|
for (; target.parentNode !== this; target = target.parentNode) {
|
|
if (target === this || !target.parentNode) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Find the child
|
|
var children = this._children;
|
|
for (var child of children) {
|
|
if (child.container === target) {
|
|
this._dnd.child = child;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!this._dnd.child) {
|
|
return;
|
|
}
|
|
|
|
if (this._dnd.delay > 0) {
|
|
this._dnd.timeout = setTimeout(() => {
|
|
this._dnd.timeout = null;
|
|
this.startDrag();
|
|
}, this._dnd.delay);
|
|
} else {
|
|
this.startDrag();
|
|
}
|
|
break;
|
|
|
|
case 'touchmove':
|
|
case 'mousemove':
|
|
var pageX, pageY, clientX, clientY;
|
|
if (event instanceof MouseEvent) {
|
|
pageX = event.pageX;
|
|
pageY = event.pageY;
|
|
clientX = event.clientX;
|
|
clientY = event.clientY;
|
|
} else {
|
|
pageX = event.touches[0].pageX;
|
|
pageY = event.touches[0].pageY;
|
|
clientX = event.touches[0].clientX;
|
|
clientY = event.touches[0].clientY;
|
|
}
|
|
|
|
if (this._dnd.timeout) {
|
|
if (Math.abs(pageX - this._dnd.start.pageX) > DND_THRESHOLD ||
|
|
Math.abs(pageY - this._dnd.start.pageY) > DND_THRESHOLD) {
|
|
clearTimeout(this._dnd.timeout);
|
|
this._dnd.timeout = null;
|
|
}
|
|
} else if (this._dnd.active) {
|
|
event.preventDefault();
|
|
this._dnd.last.pageX = pageX;
|
|
this._dnd.last.pageY = pageY;
|
|
this._dnd.last.clientX = clientX;
|
|
this._dnd.last.clientY = clientY;
|
|
this._dnd.last.timeStamp = event.timeStamp;
|
|
this.continueDrag();
|
|
}
|
|
break;
|
|
|
|
case 'touchcancel':
|
|
this.cancelDrag();
|
|
break;
|
|
|
|
case 'touchend':
|
|
case 'mouseup':
|
|
if (this._dnd.active) {
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
}
|
|
this.endDrag(event);
|
|
break;
|
|
|
|
case 'click':
|
|
if (this._dnd.clickCapture) {
|
|
this._dnd.clickCapture = false;
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
}
|
|
break;
|
|
|
|
case 'contextmenu':
|
|
if (this._dnd.active || this._dnd.timeout) {
|
|
event.stopImmediatePropagation();
|
|
event.preventDefault();
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Temporarily disables element position synchronisation. Useful when adding
|
|
* multiple elements to the container at the same time, or in quick
|
|
* succession.
|
|
*/
|
|
proto.freeze = function() {
|
|
this._frozen = true;
|
|
};
|
|
|
|
/**
|
|
* Enables element position synchronisation after freeze() has been called.
|
|
*/
|
|
proto.thaw = function() {
|
|
if (this._frozen) {
|
|
this._frozen = false;
|
|
for (var callback of this._pendingStateChanges) {
|
|
callback();
|
|
}
|
|
this._pendingStateChanges = [];
|
|
this.synchronise();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Synchronise positions between the managed container and all children.
|
|
* This is called automatically when adding/inserting or removing children,
|
|
* but must be called manually if the managed container is manipulated
|
|
* outside of these methods (for example, if style properties change, or
|
|
* if it's resized).
|
|
*/
|
|
proto.synchronise = function() {
|
|
if (this._frozen) {
|
|
return;
|
|
}
|
|
|
|
var child;
|
|
for (child of this._children) {
|
|
if (!this._dnd.active || child !== this._dnd.child) {
|
|
child.synchroniseMaster();
|
|
}
|
|
}
|
|
|
|
for (child of this._children) {
|
|
if (!this._dnd.active || child !== this._dnd.child) {
|
|
child.synchroniseContainer();
|
|
}
|
|
}
|
|
};
|
|
|
|
var template = document.createElement('template');
|
|
|
|
template.innerHTML =
|
|
`<style> :host { position: relative; display: block; }</style>` +
|
|
`<content select='*'></content>`;
|
|
|
|
function GaiaContainerChild(element) {
|
|
this._element = element;
|
|
this.markDirty();
|
|
}
|
|
|
|
GaiaContainerChild.prototype = {
|
|
get element() {
|
|
return this._element;
|
|
},
|
|
|
|
/**
|
|
* The element that will contain the child element and control its position.
|
|
*/
|
|
get container() {
|
|
if (!this._container) {
|
|
// Create container
|
|
var container = document.createElement('div');
|
|
container.classList.add('gaia-container-child');
|
|
container.style.position = 'absolute';
|
|
container.style.top = '0';
|
|
container.style.left = '0';
|
|
container.appendChild(this.element);
|
|
this._container = container;
|
|
}
|
|
|
|
return this._container;
|
|
},
|
|
|
|
/**
|
|
* The element that will be added to the container that will
|
|
* control the element's transform.
|
|
*/
|
|
get master() {
|
|
if (!this._master) {
|
|
// Create master
|
|
var master = document.createElement('div');
|
|
master.style.visibility = 'hidden';
|
|
this._master = master;
|
|
}
|
|
|
|
return this._master;
|
|
},
|
|
|
|
/**
|
|
* Clears any cached style properties. To be used if elements are
|
|
* manipulated outside of the methods of this object.
|
|
*/
|
|
markDirty() {
|
|
this._lastElementWidth = null;
|
|
this._lastElementHeight = null;
|
|
this._lastElementDisplay = null;
|
|
this._lastElementOrder = null;
|
|
this._lastMasterTop = null;
|
|
this._lastMasterLeft = null;
|
|
},
|
|
|
|
/**
|
|
* Synchronise the size of the master with the managed child element.
|
|
*/
|
|
synchroniseMaster() {
|
|
var master = this.master;
|
|
var element = this.element;
|
|
|
|
var style = window.getComputedStyle(element);
|
|
var display = style.display;
|
|
var order = style.order;
|
|
var width = element.offsetWidth;
|
|
var height = element.offsetHeight;
|
|
if (this._lastElementWidth !== width ||
|
|
this._lastElementHeight !== height ||
|
|
this._lastElementDisplay !== display ||
|
|
this._lastElementOrder !== order) {
|
|
this._lastElementWidth = width;
|
|
this._lastElementHeight = height;
|
|
this._lastElementDisplay = display;
|
|
this._lastElementOrder = order;
|
|
|
|
master.style.width = width + 'px';
|
|
master.style.height = height + 'px';
|
|
master.style.display = display;
|
|
master.style.order = order;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Synchronise the container's transform with the position of the master.
|
|
*/
|
|
synchroniseContainer() {
|
|
var master = this.master;
|
|
var container = this.container;
|
|
|
|
var top = master.offsetTop;
|
|
var left = master.offsetLeft;
|
|
|
|
if (this._lastMasterTop !== top ||
|
|
this._lastMasterLeft !== left) {
|
|
this._lastMasterTop = top;
|
|
this._lastMasterLeft = left;
|
|
|
|
container.style.transform = 'translate(' + left + 'px, ' + top + 'px)';
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.GaiaContainerChild = GaiaContainerChild;
|
|
|
|
return document.registerElement('gaia-container', { prototype: proto });
|
|
})(window);
|