infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
644 lines (566 loc) • 24.3 kB
JavaScript
/*
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/main/AUTHORS.md.
Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.
You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt
*/
;
fluid.orientation = {
HORIZONTAL: 4,
VERTICAL: 1
};
fluid.rectSides = {
// agree with fluid.orientation
4: ["left", "right"],
1: ["top", "bottom"],
// agree with fluid.direction
8: "top",
12: "bottom",
2: "left",
3: "right"
};
/**
* This is the position, relative to a given drop target, that a dragged item should be dropped.
*/
fluid.position = {
BEFORE: -1,
AFTER: 1,
INSIDE: 2,
REPLACE: 3
};
/**
* For incrementing/decrementing a count or index, or moving in a rectilinear direction.
*/
fluid.direction = {
NEXT: 1,
PREVIOUS: -1,
UP: 8,
DOWN: 12,
LEFT: 2,
RIGHT: 3
};
fluid.directionSign = function (direction) {
return direction === fluid.direction.UP || direction === fluid.direction.LEFT ?
fluid.direction.PREVIOUS : fluid.direction.NEXT;
};
fluid.directionAxis = function (direction) {
return direction === fluid.direction.LEFT || direction === fluid.direction.RIGHT ?
0 : 1;
};
fluid.directionOrientation = function (direction) {
return fluid.directionAxis(direction) ? fluid.orientation.VERTICAL : fluid.orientation.HORIZONTAL;
};
fluid.keycodeDirection = {
up: fluid.direction.UP,
down: fluid.direction.DOWN,
left: fluid.direction.LEFT,
right: fluid.direction.RIGHT
};
fluid.registerNamespace("fluid.dom");
// moves a single node in the DOM to a new position relative to another
// unsupported, NON-API function
fluid.dom.moveDom = function (source, target, position) {
source = fluid.unwrap(source);
target = fluid.unwrap(target);
var scan;
// fluid.log("moveDom source " + fluid.dumpEl(source) + " target " + fluid.dumpEl(target) + " position " + position);
if (position === fluid.position.INSIDE) {
target.appendChild(source);
} else if (position === fluid.position.BEFORE) {
for (scan = target.previousSibling;; scan = scan.previousSibling) {
if (!scan || !fluid.dom.isIgnorableNode(scan)) {
if (scan !== source) {
fluid.dom.cleanseScripts(source);
target.parentNode.insertBefore(source, target);
}
break;
}
}
} else if (position === fluid.position.AFTER) {
for (scan = target.nextSibling;; scan = scan.nextSibling) {
if (!scan || !fluid.dom.isIgnorableNode(scan)) {
if (scan !== source) {
fluid.dom.cleanseScripts(source);
fluid.dom.insertAfter(source, target);
}
break;
}
}
} else {
fluid.fail("Unrecognised position supplied to fluid.moveDom: " + position);
}
};
// unsupported, NON-API function
fluid.dom.normalisePosition = function (position, samespan, targeti, sourcei) {
// convert a REPLACE into a primitive BEFORE/AFTER
if (position === fluid.position.REPLACE) {
position = samespan && targeti >= sourcei ? fluid.position.AFTER : fluid.position.BEFORE;
}
return position;
};
fluid.dom.permuteDom = function (element, target, position, sourceelements, targetelements) {
element = fluid.unwrap(element);
target = fluid.unwrap(target);
var sourcei = $.inArray(element, sourceelements);
if (sourcei === -1) {
fluid.fail("Error in permuteDom: source element " + fluid.dumpEl(element) +
" not found in source list " + fluid.dumpEl(sourceelements));
}
var targeti = $.inArray(target, targetelements);
if (targeti === -1) {
fluid.fail("Error in permuteDom: target element " + fluid.dumpEl(target) +
" not found in source list " + fluid.dumpEl(targetelements));
}
var samespan = sourceelements === targetelements;
position = fluid.dom.normalisePosition(position, samespan, targeti, sourcei);
//fluid.log("permuteDom sourcei " + sourcei + " targeti " + targeti);
// cache the old neighbourhood of the element for the final move
var oldn = {};
oldn[fluid.position.AFTER] = element.nextSibling;
oldn[fluid.position.BEFORE] = element.previousSibling;
fluid.dom.moveDom(sourceelements[sourcei], targetelements[targeti], position);
// perform the leftward-moving, AFTER shift
var frontlimit = samespan ? targeti - 1 : sourceelements.length - 2;
var i;
if (position === fluid.position.BEFORE && samespan) {
// we cannot do skip processing if the element was "fused against the grain"
frontlimit--;
}
if (!samespan || targeti > sourcei) {
for (i = frontlimit; i > sourcei; --i) {
fluid.dom.moveDom(sourceelements[i + 1], sourceelements[i], fluid.position.AFTER);
}
if (sourcei + 1 < sourceelements.length) {
fluid.dom.moveDom(sourceelements[sourcei + 1], oldn[fluid.position.AFTER], fluid.position.BEFORE);
}
}
// perform the rightward-moving, BEFORE shift
var backlimit = samespan ? sourcei - 1 : targetelements.length - 1;
if (position === fluid.position.AFTER) {
// we cannot do skip processing if the element was "fused against the grain"
targeti++;
}
if (!samespan || targeti < sourcei) {
for (i = targeti; i < backlimit; ++i) {
fluid.dom.moveDom(targetelements[i], targetelements[i + 1], fluid.position.BEFORE);
}
if (backlimit >= 0 && backlimit < targetelements.length - 1) {
fluid.dom.moveDom(targetelements[backlimit], oldn[fluid.position.BEFORE], fluid.position.AFTER);
}
}
};
var curCss = function (a, name) {
return window.getComputedStyle ? window.getComputedStyle(a, null).getPropertyValue(name) :
a.currentStyle[name];
};
fluid.dom.isAttached = function (node) {
while (node && node.nodeName) {
if (node.nodeName === "BODY") {
return true;
}
node = node.parentNode;
}
return false;
};
fluid.dom.generalHidden = function (a) {
return "hidden" === a.type || curCss(a, "display") === "none" || curCss(a, "visibility") === "hidden" || !fluid.dom.isAttached(a);
};
fluid.registerNamespace("fluid.geometricManager");
fluid.geometricManager.computeGeometry = function (element, orientation, disposition) {
var elem = {};
elem.element = element;
elem.orientation = orientation;
if (disposition === fluid.position.INSIDE) {
elem.position = disposition;
}
if (fluid.dom.generalHidden(element)) {
elem.clazz = "hidden";
}
var pos = fluid.dom.computeAbsolutePosition(element) || [0, 0];
var width = element.offsetWidth;
var height = element.offsetHeight;
elem.rect = {left: pos[0], top: pos[1]};
elem.rect.right = pos[0] + width;
elem.rect.bottom = pos[1] + height;
return elem;
};
// A "suitable large" value for the sentinel blocks at the ends of spans
var SENTINEL_DIMENSION = 10000;
fluid.geometricManager.dumprect = function (rect) {
return "Rect top: " + rect.top +
" left: " + rect.left +
" bottom: " + rect.bottom +
" right: " + rect.right;
};
fluid.geometricManager.dumpelem = function (cacheelem) {
if (!cacheelem || !cacheelem.rect) {
return "null";
} else {
return fluid.geometricManager.dumprect(cacheelem.rect) + " position: " +
cacheelem.position +
" for " +
fluid.dumpEl(cacheelem.element);
}
};
// unsupported, NON-API function
fluid.dropManager = function () {
var targets = [];
var cache = {};
var that = {};
var lastClosest;
var lastGeometry;
var displacementX, displacementY;
that.updateGeometry = function (geometricInfo) {
lastGeometry = geometricInfo;
targets = [];
cache = {};
var mapper = geometricInfo.elementMapper;
var geometryComputor = geometricInfo.geometryComputor || fluid.geometricManager.computeGeometry;
var processElement = function (element, extent, sentB, sentF, disposition, index) {
var orientation = extent.orientation;
var sides = fluid.rectSides[orientation];
var cacheelem = geometryComputor(element, orientation, disposition);
cacheelem.owner = extent;
if (cacheelem.clazz !== "hidden" && mapper) {
cacheelem.clazz = mapper(element);
}
cache[fluid.dropManager.cacheKey(element)] = cacheelem;
var backClass = fluid.dropManager.getRelativeClass(extent.elements, index, fluid.position.BEFORE, cacheelem.clazz, mapper);
var frontClass = fluid.dropManager.getRelativeClass(extent.elements, index, fluid.position.AFTER, cacheelem.clazz, mapper);
if (disposition === fluid.position.INSIDE) {
targets[targets.length] = cacheelem;
} else {
fluid.dropManager.splitElement(targets, sides, cacheelem, disposition, backClass, frontClass);
}
// deal with sentinel blocks by creating near-copies of the end elements
if (sentB && geometricInfo.sentinelize) {
fluid.dropManager.sentinelizeElement(targets, sides, cacheelem, 1, disposition, backClass);
}
if (sentF && geometricInfo.sentinelize) {
fluid.dropManager.sentinelizeElement(targets, sides, cacheelem, 0, disposition, frontClass);
}
//fluid.log(dumpelem(cacheelem));
return cacheelem;
};
for (var i = 0; i < geometricInfo.extents.length; ++i) {
var thisInfo = geometricInfo.extents[i];
var allHidden = true;
for (var j = 0; j < thisInfo.elements.length; ++j) {
var element = thisInfo.elements[j];
var cacheelem = processElement(element, thisInfo, j === 0, j === thisInfo.elements.length - 1,
fluid.position.INTERLEAVED, j);
if (cacheelem.clazz !== "hidden") {
allHidden = false;
}
}
if (allHidden && thisInfo.parentElement) {
processElement(thisInfo.parentElement, thisInfo, true, true, fluid.position.INSIDE);
}
}
fluid.dropManager.normalizeSentinels(targets);
};
that.startDrag = function (event, handlePos, handleWidth, handleHeight) {
var handleMidX = handlePos[0] + handleWidth / 2;
var handleMidY = handlePos[1] + handleHeight / 2;
var dX = handleMidX - event.pageX;
var dY = handleMidY - event.pageY;
that.updateGeometry(lastGeometry);
lastClosest = null;
displacementX = dX;
displacementY = dY;
$("body").on("mousemove.fluid-dropManager", that.mouseMove);
};
that.lastPosition = function () {
return lastClosest;
};
that.endDrag = function () {
$("body").off("mousemove.fluid-dropManager");
};
that.mouseMove = function (evt) {
var x = evt.pageX + displacementX;
var y = evt.pageY + displacementY;
//fluid.log("Mouse x " + x + " y " + y );
var closestTarget = that.closestTarget(x, y, lastClosest);
if (closestTarget && closestTarget !== fluid.dropManager.NO_CHANGE) {
lastClosest = closestTarget;
that.dropChangeFirer.fire(closestTarget);
}
};
that.dropChangeFirer = fluid.makeEventFirer();
var blankHolder = {
element: null
};
that.closestTarget = function (x, y, lastClosest) {
var mindistance = Number.MAX_VALUE;
var minelem = blankHolder;
var minlockeddistance = Number.MAX_VALUE;
var minlockedelem = blankHolder;
for (var i = 0; i < targets.length; ++i) {
var cacheelem = targets[i];
if (cacheelem.clazz === "hidden") {
continue;
}
var distance = fluid.geom.minPointRectangle(x, y, cacheelem.rect);
if (cacheelem.clazz === "locked") {
if (distance < minlockeddistance) {
minlockeddistance = distance;
minlockedelem = cacheelem;
}
} else {
if (distance < mindistance) {
mindistance = distance;
minelem = cacheelem;
}
if (distance === 0) {
break;
}
}
}
if (!minelem) {
return minelem;
}
if (minlockeddistance >= mindistance) {
minlockedelem = blankHolder;
}
//fluid.log("PRE: mindistance " + mindistance + " element " +
// fluid.dumpEl(minelem.element) + " minlockeddistance " + minlockeddistance
// + " locked elem " + dumpelem(minlockedelem));
if (lastClosest && lastClosest.position === minelem.position &&
fluid.unwrap(lastClosest.element) === fluid.unwrap(minelem.element) &&
fluid.unwrap(lastClosest.lockedelem) === fluid.unwrap(minlockedelem.element)
) {
return fluid.dropManager.NO_CHANGE;
}
//fluid.log("mindistance " + mindistance + " minlockeddistance " + minlockeddistance);
return {
position: minelem.position,
element: minelem.element,
lockedelem: minlockedelem.element
};
};
that.shuffleProjectFrom = function (element, direction, includeLocked, disableWrap) {
var togo = that.projectFrom(element, direction, includeLocked, disableWrap);
if (togo) {
togo.position = fluid.position.REPLACE;
}
return togo;
};
that.projectFrom = function (element, direction, includeLocked, disableWrap) {
that.updateGeometry(lastGeometry);
var cacheelem = cache[fluid.dropManager.cacheKey(element)];
var projected = fluid.geom.projectFrom(cacheelem.rect, direction, targets, includeLocked, disableWrap);
if (!projected.cacheelem) {
return null;
}
var retpos = projected.cacheelem.position;
return {element: projected.cacheelem.element,
position: retpos ? retpos : fluid.position.BEFORE
};
};
that.logicalFrom = function (element, direction, includeLocked, disableWrap) {
var orderables = that.getOwningSpan(element, fluid.position.INTERLEAVED, includeLocked);
return {element: fluid.dropManager.getRelativeElement(element, direction, orderables, disableWrap),
position: fluid.position.REPLACE};
};
that.lockedWrapFrom = function (element, direction, includeLocked, disableWrap) {
var base = that.logicalFrom(element, direction, includeLocked, disableWrap);
var selectables = that.getOwningSpan(element, fluid.position.INTERLEAVED, includeLocked);
var allElements = cache[fluid.dropManager.cacheKey(element)].owner.elements;
if (includeLocked || selectables[0] === allElements[0]) {
return base;
}
var directElement = fluid.dropManager.getRelativeElement(element, direction, allElements, disableWrap);
if (lastGeometry.elementMapper(directElement) === "locked") {
base.element = null;
base.clazz = "locked";
}
return base;
};
that.getOwningSpan = function (element, position, includeLocked) {
var owner = cache[fluid.dropManager.cacheKey(element)].owner;
var elements = position === fluid.position.INSIDE ? [owner.parentElement] : owner.elements;
if (!includeLocked && lastGeometry.elementMapper) {
elements = fluid.makeArray(elements);
fluid.remove_if(elements, function (element) {
return lastGeometry.elementMapper(element) === "locked";
});
}
return elements;
};
that.geometricMove = function (element, target, position) {
var sourceElements = that.getOwningSpan(element, null, true);
var targetElements = that.getOwningSpan(target, position, true);
fluid.dom.permuteDom(element, target, position, sourceElements, targetElements);
};
return that;
};
fluid.dropManager.NO_CHANGE = "no change";
fluid.dropManager.cacheKey = function (element) {
return fluid.allocateSimpleId(element);
};
fluid.dropManager.sentinelizeElement = function (targets, sides, cacheelem, fc, disposition, clazz) {
var elemCopy = $.extend(true, {}, cacheelem);
elemCopy.origRect = fluid.copy(elemCopy.rect);
elemCopy.rect[sides[fc]] = elemCopy.rect[sides[1 - fc]] + (fc ? 1 : -1);
elemCopy.rect[sides[1 - fc]] = (fc ? -1 : 1) * SENTINEL_DIMENSION;
elemCopy.position = disposition === fluid.position.INSIDE ?
disposition : (fc ? fluid.position.BEFORE : fluid.position.AFTER);
elemCopy.clazz = clazz;
targets[targets.length] = elemCopy;
};
// This function is necessary to prevent overlapping sentinels for FLUID-4692
// Very sadly this simple implementation now makes the setup O(n^2) in the number of elements
fluid.dropManager.normalizeSentinels = function (targets) {
for (var i = 0; i < targets.length; ++i) {
for (var j = 0; j < targets.length; ++j) {
var ti = targets[i], tj = targets[j];
var jrect = tj.origRect || tj.rect;
if (ti.element !== tj.element && ti.origRect && fluid.geom.minRectRect(ti.rect, jrect) === 0) {
ti.rect = ti.origRect;
delete ti.origRect;
}
}
}
};
fluid.dropManager.splitElement = function (targets, sides, cacheelem, disposition, clazz1, clazz2) {
var elem1 = $.extend(true, {}, cacheelem);
var elem2 = $.extend(true, {}, cacheelem);
var midpoint = (elem1.rect[sides[0]] + elem1.rect[sides[1]]) / 2;
elem1.rect[sides[1]] = midpoint;
elem1.position = fluid.position.BEFORE;
elem2.rect[sides[0]] = midpoint;
elem2.position = fluid.position.AFTER;
elem1.clazz = clazz1;
elem2.clazz = clazz2;
targets[targets.length] = elem1;
targets[targets.length] = elem2;
};
// Expand this configuration point if we ever go back to a full "permissions" model
fluid.dropManager.getRelativeClass = function (thisElements, index, relative, thisclazz, mapper) {
index += relative;
if (index < 0 && thisclazz === "locked") {
return "locked";
}
if (index >= thisElements.length || mapper === null) {
return null;
} else {
relative = thisElements[index];
return mapper(relative) === "locked" && thisclazz === "locked" ? "locked" : null;
}
};
fluid.dropManager.getRelativeElement = function (element, direction, elements, disableWrap) {
var folded = fluid.directionSign(direction);
var index = $(elements).index(element) + folded;
if (index < 0) {
index += elements.length;
}
// disable wrap
if (disableWrap) {
if (index === elements.length || index === (elements.length + folded)) {
return element;
}
}
index %= elements.length;
return elements[index];
};
fluid.geom = fluid.geom || {};
// These distance algorithms have been taken from
// http://www.cs.mcgill.ca/~cs644/Godfried/2005/Fall/fzamal/concepts.htm
/* Returns the minimum squared distance between a point and a rectangle */
fluid.geom.minPointRectangle = function (x, y, rectangle) {
var dx = x < rectangle.left ? (rectangle.left - x) :
(x > rectangle.right ? (x - rectangle.right) : 0);
var dy = y < rectangle.top ? (rectangle.top - y) :
(y > rectangle.bottom ? (y - rectangle.bottom) : 0);
return dx * dx + dy * dy;
};
/* Returns the minimum squared distance between two rectangles */
fluid.geom.minRectRect = function (rect1, rect2) {
var dx = rect1.right < rect2.left ? rect2.left - rect1.right :
rect2.right < rect1.left ? rect1.left - rect2.right : 0;
var dy = rect1.bottom < rect2.top ? rect2.top - rect1.bottom :
rect2.bottom < rect1.top ? rect1.top - rect2.bottom : 0;
return dx * dx + dy * dy;
};
var makePenCollect = function () {
return {
mindist: Number.MAX_VALUE,
minrdist: Number.MAX_VALUE
};
};
/**
* Determine the one amongst a set of rectangle targets which is the "best fit"
* for an axial motion from a "base rectangle" (commonly arising from the case
* of cursor key navigation).
*
* @param {Rectangle} baserect - The base rectangle from which the motion is to be referred.
* @param {Object} direction - The direction of motion, which should be an instance of fluid.direction.
* @param {Array} targets - An array of objects "cache elements" for which the member <code>rect</code> is the
* holder of the rectangle to be tested.
* @param {Boolean} forSelection - Set to `true` to indicate that we are dealing with a selection.
* @param {Boolean} disableWrap - Set to `true` to disable wrapping of elements.
* @return {Object} - The cache element which is the most appropriate for the requested motion.
*/
fluid.geom.projectFrom = function (baserect, direction, targets, forSelection, disableWrap) {
var axis = fluid.directionAxis(direction);
var frontSide = fluid.rectSides[direction];
var backSide = fluid.rectSides[axis * 15 + 5 - direction];
var dirSign = fluid.directionSign(direction);
var penrect = {
left: (7 * baserect.left + 1 * baserect.right) / 8,
right: (5 * baserect.left + 3 * baserect.right) / 8,
top: (7 * baserect.top + 1 * baserect.bottom) / 8,
bottom: (5 * baserect.top + 3 * baserect.bottom) / 8
};
penrect[frontSide] = dirSign * SENTINEL_DIMENSION;
penrect[backSide] = -penrect[frontSide];
function accPen(collect, cacheelem, backSign) {
var thisrect = cacheelem.rect;
var pdist = fluid.geom.minRectRect(penrect, thisrect);
var rdist = -dirSign * backSign * (baserect[backSign === 1 ? frontSide : backSide] -
thisrect[backSign === 1 ? backSide : frontSide]);
// fluid.log("pdist: " + pdist + " rdist: " + rdist);
// the oddity in the rdist comparison is intended to express "half-open"-ness of rectangles
// (backSign === 1 ? 0 : 1) - this is now gone - must be possible to move to perpendicularly abutting regions
if (pdist <= collect.mindist && rdist >= 0) {
if (pdist === collect.mindist && rdist * backSign > collect.minrdist) {
return;
}
collect.minrdist = rdist * backSign;
collect.mindist = pdist;
collect.minelem = cacheelem;
}
}
var collect = makePenCollect();
var backcollect = makePenCollect();
var lockedcollect = makePenCollect();
for (var i = 0; i < targets.length; ++i) {
var elem = targets[i];
var isPure = elem.owner && elem.element === elem.owner.parentElement;
if (elem.clazz === "hidden" || (forSelection && isPure)) {
continue;
} else if (!forSelection && elem.clazz === "locked") {
accPen(lockedcollect, elem, 1);
} else {
accPen(collect, elem, 1);
accPen(backcollect, elem, -1);
}
//fluid.log("Element " + i + " " + dumpelem(elem) + " mindist " + collect.mindist);
}
var wrap = !collect.minelem || backcollect.mindist < collect.mindist;
// disable wrap
wrap = wrap && !disableWrap;
var mincollect = wrap ? backcollect : collect;
var togo = {
wrapped: wrap,
cacheelem: mincollect.minelem
};
if (lockedcollect.mindist < mincollect.mindist) {
togo.lockedelem = lockedcollect.minelem;
}
return togo;
};