This commit is contained in:
yinsx
2026-02-02 09:07:30 +08:00
parent e04353a5fa
commit bf5a3bc343
78 changed files with 11771 additions and 318 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup lang="ts">
import { computed } from 'vue';
import type { Folder } from '@/store/useFoldersStore';
@ -27,8 +27,8 @@ const initialText = (child: FolderChild) => {
</script>
<template>
<div class="folder-card-wrapper">
<div class="folder-card" @click.stop="emit('open')">
<div class="folder-badge">{{ props.folder.name || '组' }}</div>
<div class="folder-preview">
<div
v-for="child in previewChildren"
@ -43,11 +43,19 @@ const initialText = (child: FolderChild) => {
<div v-if="!previewChildren.length" class="folder-empty"></div>
</div>
</div>
<div class="folder-label">{{ props.folder.name || '文件夹' }}</div>
</div>
</template>
<style scoped lang="scss">
@import '@/styles/tokens.scss';
.folder-card-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.folder-card {
width: 100%;
height: 100%;
@ -66,19 +74,6 @@ const initialText = (child: FolderChild) => {
}
}
.folder-badge {
position: absolute;
top: 8px;
right: 10px;
background: rgba(0, 122, 255, 0.9);
color: #fff;
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
font-weight: 600;
pointer-events: none;
}
.folder-preview {
width: 100%;
height: 100%;
@ -123,4 +118,21 @@ const initialText = (child: FolderChild) => {
justify-content: center;
font-size: 13px;
}
.folder-label {
position: absolute;
top: 100%;
left: 50%;
margin-top: var(--icon-label-margin-top);
transform: translateX(-50%);
font-size: var(--icon-label-font-size);
color: $color-text-primary;
text-align: center;
width: 100%;
max-width: 100%;
visibility: var(--icon-label-visibility, visible);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
</style>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import type { Folder } from '@/store/useFoldersStore';
type FolderChild =
@ -19,23 +19,57 @@ const emit = defineEmits<{
(e: 'open-item', payload: FolderChild): void;
}>();
const localName = ref('');
const localName = ref('文件夹');
const editingName = ref(false);
const nameInputRef = ref<HTMLInputElement | null>(null);
watch(
() => props.folder?.name,
name => {
localName.value = name ?? '';
localName.value = name ?? '文件夹';
},
{ immediate: true }
);
watch(
() => props.folder?.id,
() => {
editingName.value = false;
}
);
watch(
() => props.open,
() => {
editingName.value = false;
}
);
const handleRename = () => {
if (!props.folder) return;
const name = localName.value.trim() || '';
const name = localName.value.trim() || '文件夹';
emit('rename', { id: props.folder.id, name });
};
const badgeLabel = computed(() => props.folder?.name ?? '组');
const focusRenameInput = async () => {
await nextTick();
const input = nameInputRef.value;
if (!input) return;
input.focus();
const len = input.value.length;
input.setSelectionRange(len, len);
};
const startRename = () => {
editingName.value = true;
};
const finishRename = () => {
editingName.value = false;
handleRename();
};
const badgeLabel = computed(() => localName.value || '文件夹');
const initialText = (child: FolderChild) => {
if (child.type === 'icon') {
@ -44,56 +78,66 @@ const initialText = (child: FolderChild) => {
}
return 'W';
};
const labelFor = (child: FolderChild) =>
child.type === 'icon' ? child.icon?.name ?? '未命名' : child.widget?.component ?? 'Widget';
</script>
<template>
<teleport to="body">
<div v-if="open && folder" class="folder-dialog-overlay" @click.self="emit('close')">
<div class="folder-dialog">
<header class="folder-dialog__header">
<div
class="folder-dialog-overlay"
:class="{ 'is-open': open && folder }"
@click.self="emit('close')"
>
<transition name="folder-dialog">
<div v-if="open && folder" class="folder-dialog-wrap">
<div class="folder-title">
<transition name="folder-title" mode="out-in" @after-enter="focusRenameInput">
<button v-if="!editingName" key="view" type="button" class="folder-title-pill" @click="startRename">
{{ badgeLabel }}
</button>
<input
v-else
key="edit"
ref="nameInputRef"
v-model="localName"
class="folder-name-input"
@keyup.enter="handleRename"
@blur="handleRename"
class="folder-title-input"
@keyup.enter="finishRename"
@blur="finishRename"
/>
<span class="folder-badge">{{ badgeLabel }}</span>
<button class="close-btn" type="button" @click="emit('close')">×</button>
</header>
<div class="folder-dialog__body">
</transition>
</div>
<div class="folder-dialog">
<div v-if="children.length" class="folder-grid">
<div
v-for="child in children"
:key="`${child.type}-${child.id}`"
class="folder-row"
class="folder-item"
@click="emit('open-item', child)"
>
<div
class="row-thumb"
class="folder-item-icon"
:data-type="child.type"
:style="child.type === 'icon' && child.icon?.bgColor ? { backgroundColor: child.icon.bgColor } : {}"
>
<img v-if="child.type === 'icon' && child.icon?.img" :src="child.icon.img" :alt="child.icon?.name" />
<span v-else>{{ initialText(child) }}</span>
</div>
<div class="row-title">
<div class="row-name">
{{ child.type === 'icon' ? child.icon?.name : child.widget?.component || 'Widget' }}
</div>
<div class="row-sub" v-if="child.type === 'icon' && child.icon?.url">{{ child.icon.url }}</div>
</div>
<div class="row-actions">
<button type="button" class="link-btn" @click="emit('open-item', child)">打开</button>
<div class="folder-item-label">{{ labelFor(child) }}</div>
<button
type="button"
class="link-btn danger"
@click="emit('remove', { folderId: folder.id, child })"
class="folder-item-remove"
@click.stop="emit('remove', { folderId: folder.id, child })"
>
移出
</button>
</div>
</div>
<div v-if="!children.length" class="folder-empty">还没有内容</div>
<div v-else class="folder-empty"></div>
</div>
</div>
</transition>
</div>
</teleport>
</template>
@ -104,96 +148,119 @@ const initialText = (child: FolderChild) => {
.folder-dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(6px);
background: rgba(10, 12, 20, 0.32);
backdrop-filter: blur(18px) saturate(140%);
-webkit-backdrop-filter: blur(18px) saturate(140%);
display: flex;
align-items: center;
justify-content: center;
z-index: $z-index-menu;
opacity: 0;
pointer-events: none;
}
.folder-dialog-overlay.is-open {
opacity: 1;
pointer-events: auto;
}
.folder-dialog-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.folder-title {
display: flex;
align-items: center;
justify-content: center;
}
.folder-title-pill {
min-width: 180px;
height: 36px;
padding: 0 16px;
border-radius: 10px;
border: none;
background: transparent;
color: #fff;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
box-shadow: none;
cursor: pointer;
box-sizing: border-box;
}
.folder-title-input {
min-width: 180px;
height: 36px;
padding: 0 16px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.35);
background: rgba(255, 255, 255, 0.38);
backdrop-filter: blur(22px) saturate(160%);
-webkit-backdrop-filter: blur(22px) saturate(160%);
color: rgba(0, 0, 0, 0.85);
font-size: 16px;
font-weight: 600;
outline: none;
text-align: center;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
.folder-dialog {
width: min(520px, 90vw);
max-height: 80vh;
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 80px rgba(0, 0, 0, 0.18);
overflow: hidden;
display: flex;
flex-direction: column;
width: min(720px, 90vw);
min-height: 200px;
padding: 22px 26px 26px;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.35);
background: rgba(255, 255, 255, 0.38);
backdrop-filter: blur(22px) saturate(160%);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.2);
}
.folder-dialog__header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 18px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.folder-name-input {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 10px;
padding: 8px 10px;
font-size: 15px;
outline: none;
transition: border-color $motion-duration-sm $motion-easing-standard, box-shadow $motion-duration-sm $motion-easing-standard;
&:focus {
border-color: #007aff;
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
}
}
.folder-badge {
background: rgba(0, 122, 255, 0.12);
color: #0a63d1;
border-radius: 999px;
padding: 6px 12px;
font-weight: 600;
font-size: 13px;
}
.close-btn {
background: transparent;
border: none;
font-size: 20px;
color: #666;
cursor: pointer;
line-height: 1;
}
.folder-dialog__body {
padding: 12px 18px 18px;
overflow: auto;
max-height: 60vh;
display: flex;
flex-direction: column;
gap: 10px;
}
.folder-row {
.folder-grid {
display: grid;
grid-template-columns: 54px 1fr auto;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
background: rgba(0, 0, 0, 0.02);
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 16px;
}
.row-thumb {
width: 54px;
height: 54px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.06);
.folder-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 6px 4px 10px;
border-radius: 14px;
cursor: pointer;
transition: transform $motion-duration-sm $motion-easing-standard, background $motion-duration-sm $motion-easing-standard;
&:hover {
transform: translateY(-2px);
background: rgba(255, 255, 255, 0.2);
}
&:hover .folder-item-remove {
opacity: 1;
transform: translateY(0);
}
}
.folder-item-icon {
width: 56px;
height: 56px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 18px;
color: #222;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
overflow: hidden;
img {
@ -207,62 +274,56 @@ const initialText = (child: FolderChild) => {
}
}
.row-title {
min-width: 0;
}
.row-name {
font-weight: 600;
color: #1f1f1f;
font-size: 15px;
line-height: 1.3;
}
.row-sub {
.folder-item-label {
font-size: 12px;
color: #666;
margin-top: 4px;
color: rgba(0, 0, 0, 0.7);
text-align: center;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row-actions {
display: flex;
align-items: center;
gap: 8px;
}
.link-btn {
background: rgba(0, 0, 0, 0.05);
.folder-item-remove {
position: absolute;
top: 2px;
right: 6px;
border: none;
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
font-size: 13px;
color: #222;
transition: background $motion-duration-sm $motion-easing-standard, transform $motion-duration-sm $motion-easing-standard;
&:hover {
background: rgba(0, 122, 255, 0.12);
transform: translateY(-1px);
}
&.danger {
background: rgba(255, 255, 255, 0.85);
color: #c0392b;
background: rgba(192, 57, 43, 0.08);
&:hover {
background: rgba(192, 57, 43, 0.15);
}
}
border-radius: 999px;
padding: 2px 8px;
font-size: 11px;
cursor: pointer;
opacity: 0;
transform: translateY(-4px);
transition: opacity $motion-duration-sm $motion-easing-standard, transform $motion-duration-sm $motion-easing-standard;
}
.folder-empty {
text-align: center;
padding: 26px 12px;
color: #666;
border: 1px dashed rgba(0, 0, 0, 0.08);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
min-height: 160px;
color: rgba(255, 255, 255, 0.85);
font-weight: 600;
}
.folder-dialog-enter-active {
transition: transform 220ms $motion-easing-standard;
}
.folder-dialog-enter-from {
transform: translateY(14px) scale(0.98);
}
.folder-title-enter-active,
.folder-title-leave-active {
transition: opacity 90ms $motion-easing-standard;
}
.folder-title-enter-from,
.folder-title-leave-to {
opacity: 0;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,18 @@
<li @click="handleEditWidget"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="menu-icon"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path></svg>编辑组件</li>
<li class="menu-danger" @click="deleteItem"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="menu-icon"><path d="M3 6h18"></path><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>删除</li>
</ul>
<ul v-else-if="uiStore.contextMenu.itemType === 'folder'">
<li class="layout-section">
<div class="layout-title">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="menu-icon"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
<span>布局</span>
</div>
<div class="widget-size-options">
<span v-for="size in folderSizes" :key="size" @click="changeFolderSize(size)" class="size-option">{{ size }}</span>
</div>
</li>
<li class="menu-danger" @click="deleteItem"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="menu-icon"><path d="M3 6h18"></path><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>删除文件夹</li>
</ul>
<ul v-else-if="uiStore.contextMenu.itemType === 'desktop'" class="desktop-menu">
<li class="desktop-item" @click="handleDesktopAction('add-icon')">
<span class="desktop-label">添加图标</span>
@ -74,6 +86,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useUIStore } from '@/store/useUIStore';
import { useLayoutStore } from '@/store/useLayoutStore';
import { useWidgetsStore } from '@/store/useWidgetsStore';
import { useFoldersStore } from '@/store/useFoldersStore';
const emit = defineEmits<{
(event: 'add-icon'): void;
@ -85,6 +98,7 @@ const emit = defineEmits<{
const uiStore = useUIStore();
const layoutStore = useLayoutStore();
const widgetsStore = useWidgetsStore();
const foldersStore = useFoldersStore();
type ItemSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
@ -92,6 +106,7 @@ type DesktopAction = 'add-icon' | 'wallpaper' | 'search' | 'backup' | 'settings'
const widgetSizes: ItemSize[] = ['1x1', '1x2', '2x1', '2x2', '2x4'];
const iconSizes: ItemSize[] = ['1x1', '1x2', '2x1', '2x2', '2x4'];
const folderSizes: ItemSize[] = ['1x1', '1x2', '2x1', '2x2', '2x4'];
const menuRef = ref<HTMLElement | null>(null);
const menuPosition = ref({ x: 0, y: 0 });
@ -147,9 +162,11 @@ const close = () => {
const deleteItem = () => {
if (uiStore.contextMenu.itemId) {
if (uiStore.contextMenu.itemType === 'icon') {
layoutStore.deleteIcon(uiStore.contextMenu.itemId);
uiStore.requestDelete(uiStore.contextMenu.itemId, 'icon');
} else if (uiStore.contextMenu.itemType === 'widget') {
widgetsStore.deleteWidget(uiStore.contextMenu.itemId);
uiStore.requestDelete(uiStore.contextMenu.itemId, 'widget');
} else if (uiStore.contextMenu.itemType === 'folder') {
uiStore.requestDelete(uiStore.contextMenu.itemId, 'folder');
}
}
close();
@ -185,6 +202,13 @@ const changeIconSize = (newSize: ItemSize) => {
close();
};
const changeFolderSize = (newSize: ItemSize) => {
if (uiStore.contextMenu.itemId) {
foldersStore.updateFolderSize(uiStore.contextMenu.itemId, newSize);
}
close();
};
const handleDesktopAction = (action: DesktopAction) => {
if (action === 'add-icon') {
emit('add-icon');

View File

@ -1,6 +1,7 @@
import { defineStore, acceptHMRUpdate } from 'pinia';
import { defineStore, acceptHMRUpdate } from 'pinia';
type FolderItemType = 'icon' | 'widget';
type FolderSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
export interface FolderItem {
id: string;
@ -12,6 +13,7 @@ export interface Folder {
name: string;
items: FolderItem[];
groupId?: string;
size?: FolderSize;
}
interface FolderState {
@ -21,10 +23,17 @@ interface FolderState {
const DEFAULT_GROUP_ID = 'home';
const STORAGE_KEY = 'itab_folders';
const normalizeFolderName = (name?: string) => {
const trimmed = (name ?? '').trim();
if (!trimmed || trimmed === '组') return '文件夹';
return trimmed;
};
const normalizeFolder = (folder: Folder): Folder => ({
id: folder.id,
name: folder.name || '组',
name: normalizeFolderName(folder.name),
groupId: folder.groupId ?? DEFAULT_GROUP_ID,
size: folder.size ?? '1x1',
items: Array.isArray(folder.items)
? folder.items
.filter(item => item && typeof item.id === 'string' && (item.type === 'icon' || item.type === 'widget'))
@ -52,13 +61,14 @@ export const useFoldersStore = defineStore('folders', {
persist() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.folders));
},
addFolder(payload: { name?: string; items?: FolderItem[]; groupId?: string }) {
addFolder(payload: { name?: string; items?: FolderItem[]; groupId?: string; size?: FolderSize }) {
const nextId = `folder-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
const folder: Folder = normalizeFolder({
id: nextId,
name: payload.name || '组',
name: normalizeFolderName(payload.name),
items: payload.items ?? [],
groupId: payload.groupId ?? DEFAULT_GROUP_ID,
size: payload.size ?? '1x1',
});
this.folders.push(folder);
this.persist();
@ -67,7 +77,13 @@ export const useFoldersStore = defineStore('folders', {
renameFolder(folderId: string, name: string) {
const folder = this.folders.find(item => item.id === folderId);
if (!folder) return;
folder.name = name || '组';
folder.name = normalizeFolderName(name);
this.persist();
},
updateFolderSize(folderId: string, size: FolderSize) {
const folder = this.folders.find(item => item.id === folderId);
if (!folder) return;
folder.size = size;
this.persist();
},
addItem(folderId: string, item: FolderItem) {

View File

@ -5,11 +5,12 @@ interface ContextMenuState {
x: number;
y: number;
itemId: string | null;
itemType?: 'icon' | 'widget' | 'desktop';
itemType?: 'icon' | 'widget' | 'folder' | 'desktop';
}
interface UIState {
contextMenu: ContextMenuState;
pendingDelete: { id: string; type: 'icon' | 'widget' | 'folder' } | null;
}
export const useUIStore = defineStore('ui', {
@ -20,14 +21,15 @@ export const useUIStore = defineStore('ui', {
y: 0,
itemId: null,
itemType: 'icon',
}
},
pendingDelete: null,
}),
actions: {
openContextMenu(
x: number,
y: number,
itemId: string | null,
itemType: 'icon' | 'widget' | 'desktop'
itemType: 'icon' | 'widget' | 'folder' | 'desktop'
) {
this.contextMenu = {
isOpen: true,
@ -41,5 +43,11 @@ export const useUIStore = defineStore('ui', {
this.contextMenu.isOpen = false;
this.contextMenu.itemId = null;
},
requestDelete(id: string, type: 'icon' | 'widget' | 'folder') {
this.pendingDelete = { id, type };
},
clearPendingDelete() {
this.pendingDelete = null;
},
},
});

View File

@ -0,0 +1,218 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getCurrentStyles from '../utils/getCurrentStyles';
import getUnprefixedPropName from '../utils/getUnprefixedPropName';
import isFunction from '../utils/isFunction';
import isNative from '../utils/isNative';
import setStyles from '../utils/setStyles';
var HAS_WEB_ANIMATIONS = !!(Element && isFunction(Element.prototype.animate));
var HAS_NATIVE_WEB_ANIMATIONS = !!(Element && isNative(Element.prototype.animate));
/**
* Item animation handler powered by Web Animations API.
*
* @class
* @param {HTMLElement} element
*/
function Animator(element) {
this._element = element;
this._animation = null;
this._duration = 0;
this._easing = '';
this._callback = null;
this._props = [];
this._values = [];
this._isDestroyed = false;
this._onFinish = this._onFinish.bind(this);
}
/**
* Public prototype methods
* ************************
*/
/**
* Start instance's animation. Automatically stops current animation if it is
* running.
*
* @public
* @param {Object} propsFrom
* @param {Object} propsTo
* @param {Object} [options]
* @param {Number} [options.duration=300]
* @param {String} [options.easing='ease']
* @param {Function} [options.onFinish]
*/
Animator.prototype.start = function (propsFrom, propsTo, options) {
if (this._isDestroyed) return;
var element = this._element;
var opts = options || {};
// If we don't have web animations available let's not animate.
if (!HAS_WEB_ANIMATIONS) {
setStyles(element, propsTo);
this._callback = isFunction(opts.onFinish) ? opts.onFinish : null;
this._onFinish();
return;
}
var animation = this._animation;
var currentProps = this._props;
var currentValues = this._values;
var duration = opts.duration || 300;
var easing = opts.easing || 'ease';
var cancelAnimation = false;
var propName, propCount, propIndex;
// If we have an existing animation running, let's check if it needs to be
// cancelled or if it can continue running.
if (animation) {
propCount = 0;
// Cancel animation if duration or easing has changed.
if (duration !== this._duration || easing !== this._easing) {
cancelAnimation = true;
}
// Check if the requested animation target props and values match with the
// current props and values.
if (!cancelAnimation) {
for (propName in propsTo) {
++propCount;
propIndex = currentProps.indexOf(propName);
if (propIndex === -1 || propsTo[propName] !== currentValues[propIndex]) {
cancelAnimation = true;
break;
}
}
// Check if the target props count matches current props count. This is
// needed for the edge case scenario where target props contain the same
// styles as current props, but the current props have some additional
// props.
if (propCount !== currentProps.length) {
cancelAnimation = true;
}
}
}
// Cancel animation (if required).
if (cancelAnimation) animation.cancel();
// Store animation callback.
this._callback = isFunction(opts.onFinish) ? opts.onFinish : null;
// If we have a running animation that does not need to be cancelled, let's
// call it a day here and let it run.
if (animation && !cancelAnimation) return;
// Store target props and values to instance.
currentProps.length = currentValues.length = 0;
for (propName in propsTo) {
currentProps.push(propName);
currentValues.push(propsTo[propName]);
}
// Start the animation. We need to provide unprefixed property names to the
// Web Animations polyfill if it is being used. If we have native Web
// Animations available we need to provide prefixed properties instead.
this._duration = duration;
this._easing = easing;
this._animation = element.animate(
[
createFrame(propsFrom, HAS_NATIVE_WEB_ANIMATIONS),
createFrame(propsTo, HAS_NATIVE_WEB_ANIMATIONS),
],
{
duration: duration,
easing: easing,
}
);
this._animation.onfinish = this._onFinish;
// Set the end styles. This makes sure that the element stays at the end
// values after animation is finished.
setStyles(element, propsTo);
};
/**
* Stop instance's current animation if running.
*
* @public
*/
Animator.prototype.stop = function () {
if (this._isDestroyed || !this._animation) return;
this._animation.cancel();
this._animation = this._callback = null;
this._props.length = this._values.length = 0;
};
/**
* Read the current values of the element's animated styles from the DOM.
*
* @public
* @return {Object}
*/
Animator.prototype.getCurrentStyles = function () {
return getCurrentStyles(element, currentProps);
};
/**
* Check if the item is being animated currently.
*
* @public
* @return {Boolean}
*/
Animator.prototype.isAnimating = function () {
return !!this._animation;
};
/**
* Destroy the instance and stop current animation if it is running.
*
* @public
*/
Animator.prototype.destroy = function () {
if (this._isDestroyed) return;
this.stop();
this._element = null;
this._isDestroyed = true;
};
/**
* Private prototype methods
* *************************
*/
/**
* Animation end handler.
*
* @private
*/
Animator.prototype._onFinish = function () {
var callback = this._callback;
this._animation = this._callback = null;
this._props.length = this._values.length = 0;
callback && callback();
};
/**
* Private helpers
* ***************
*/
function createFrame(props, prefix) {
var frame = {};
for (var prop in props) {
frame[prefix ? prop : getUnprefixedPropName(prop)] = props[prop];
}
return frame;
}
export default Animator;

View File

@ -0,0 +1,740 @@
/**
* Muuri AutoScroller
* Copyright (c) 2019-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md
*/
import { addAutoScrollTick, cancelAutoScrollTick } from '../ticker';
import { LEFT, RIGHT, UP, DOWN, AXIS_X, AXIS_Y, FORWARD, BACKWARD } from './constants';
import ScrollRequest from './ScrollRequest';
import ScrollAction from './ScrollAction';
import Pool from './Pool';
import getIntersectionScore from '../utils/getIntersectionScore';
import isFunction from '../utils/isFunction';
import {
getScrollElement,
getScrollLeft,
getScrollTop,
getScrollLeftMax,
getScrollTopMax,
getContentRect,
getItemAutoScrollSettings,
prepareItemScrollSync,
applyItemScrollSync,
computeThreshold,
} from './utils';
var RECT_1 = {
width: 0,
height: 0,
left: 0,
right: 0,
top: 0,
bottom: 0,
};
var RECT_2 = {
width: 0,
height: 0,
left: 0,
right: 0,
top: 0,
bottom: 0,
};
export default function AutoScroller() {
this._isDestroyed = false;
this._isTicking = false;
this._tickTime = 0;
this._tickDeltaTime = 0;
this._items = [];
this._actions = [];
this._requests = {};
this._requests[AXIS_X] = {};
this._requests[AXIS_Y] = {};
this._requestOverlapCheck = {};
this._dragPositions = {};
this._dragDirections = {};
this._overlapCheckInterval = 150;
this._requestPool = new Pool(
function () {
return new ScrollRequest();
},
function (request) {
request.reset();
}
);
this._actionPool = new Pool(
function () {
return new ScrollAction();
},
function (action) {
action.reset();
}
);
this._readTick = this._readTick.bind(this);
this._writeTick = this._writeTick.bind(this);
}
AutoScroller.AXIS_X = AXIS_X;
AutoScroller.AXIS_Y = AXIS_Y;
AutoScroller.FORWARD = FORWARD;
AutoScroller.BACKWARD = BACKWARD;
AutoScroller.LEFT = LEFT;
AutoScroller.RIGHT = RIGHT;
AutoScroller.UP = UP;
AutoScroller.DOWN = DOWN;
AutoScroller.smoothSpeed = function (maxSpeed, acceleration, deceleration) {
return function (item, element, data) {
var targetSpeed = 0;
if (!data.isEnding) {
if (data.threshold > 0) {
var factor = data.threshold - Math.max(0, data.distance);
targetSpeed = (maxSpeed / data.threshold) * factor;
} else {
targetSpeed = maxSpeed;
}
}
var currentSpeed = data.speed;
var nextSpeed = targetSpeed;
if (currentSpeed === targetSpeed) {
return nextSpeed;
}
if (currentSpeed < targetSpeed) {
nextSpeed = currentSpeed + acceleration * (data.deltaTime / 1000);
return Math.min(targetSpeed, nextSpeed);
} else {
nextSpeed = currentSpeed - deceleration * (data.deltaTime / 1000);
return Math.max(targetSpeed, nextSpeed);
}
};
};
AutoScroller.pointerHandle = function (pointerSize) {
var rect = { left: 0, top: 0, width: 0, height: 0 };
var size = pointerSize || 1;
return function (item, x, y, w, h, pX, pY) {
rect.left = pX - size * 0.5;
rect.top = pY - size * 0.5;
rect.width = size;
rect.height = size;
return rect;
};
};
AutoScroller.prototype._readTick = function (time) {
if (this._isDestroyed) return;
if (time && this._tickTime) {
this._tickDeltaTime = time - this._tickTime;
this._tickTime = time;
this._updateRequests();
this._updateActions();
} else {
this._tickTime = time;
this._tickDeltaTime = 0;
}
};
AutoScroller.prototype._writeTick = function () {
if (this._isDestroyed) return;
this._applyActions();
addAutoScrollTick(this._readTick, this._writeTick);
};
AutoScroller.prototype._startTicking = function () {
this._isTicking = true;
addAutoScrollTick(this._readTick, this._writeTick);
};
AutoScroller.prototype._stopTicking = function () {
this._isTicking = false;
this._tickTime = 0;
this._tickDeltaTime = 0;
cancelAutoScrollTick();
};
AutoScroller.prototype._getItemHandleRect = function (item, handle, rect) {
var itemDrag = item._drag;
if (handle) {
var ev = itemDrag._dragMoveEvent || itemDrag._dragStartEvent;
var data = handle(
item,
itemDrag._clientX,
itemDrag._clientY,
item._width,
item._height,
ev.clientX,
ev.clientY
);
rect.left = data.left;
rect.top = data.top;
rect.width = data.width;
rect.height = data.height;
} else {
rect.left = itemDrag._clientX;
rect.top = itemDrag._clientY;
rect.width = item._width;
rect.height = item._height;
}
rect.right = rect.left + rect.width;
rect.bottom = rect.top + rect.height;
return rect;
};
AutoScroller.prototype._requestItemScroll = function (
item,
axis,
element,
direction,
threshold,
distance,
maxValue
) {
var reqMap = this._requests[axis];
var request = reqMap[item._id];
if (request) {
if (request.element !== element || request.direction !== direction) {
request.reset();
}
} else {
request = this._requestPool.pick();
}
request.item = item;
request.element = element;
request.direction = direction;
request.threshold = threshold;
request.distance = distance;
request.maxValue = maxValue;
reqMap[item._id] = request;
};
AutoScroller.prototype._cancelItemScroll = function (item, axis) {
var reqMap = this._requests[axis];
var request = reqMap[item._id];
if (!request) return;
if (request.action) request.action.removeRequest(request);
this._requestPool.release(request);
delete reqMap[item._id];
};
AutoScroller.prototype._checkItemOverlap = function (item, checkX, checkY) {
var settings = getItemAutoScrollSettings(item);
var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets;
var threshold = settings.threshold;
var safeZone = settings.safeZone;
if (!targets || !targets.length) {
checkX && this._cancelItemScroll(item, AXIS_X);
checkY && this._cancelItemScroll(item, AXIS_Y);
return;
}
var dragDirections = this._dragDirections[item._id];
var dragDirectionX = dragDirections[0];
var dragDirectionY = dragDirections[1];
if (!dragDirectionX && !dragDirectionY) {
checkX && this._cancelItemScroll(item, AXIS_X);
checkY && this._cancelItemScroll(item, AXIS_Y);
return;
}
var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1);
var testRect = RECT_2;
var target = null;
var testElement = null;
var testAxisX = true;
var testAxisY = true;
var testScore = 0;
var testPriority = 0;
var testThreshold = null;
var testDirection = null;
var testDistance = 0;
var testMaxScrollX = 0;
var testMaxScrollY = 0;
var xElement = null;
var xPriority = -Infinity;
var xThreshold = 0;
var xScore = 0;
var xDirection = null;
var xDistance = 0;
var xMaxScroll = 0;
var yElement = null;
var yPriority = -Infinity;
var yThreshold = 0;
var yScore = 0;
var yDirection = null;
var yDistance = 0;
var yMaxScroll = 0;
for (var i = 0; i < targets.length; i++) {
target = targets[i];
testAxisX = checkX && dragDirectionX && target.axis !== AXIS_Y;
testAxisY = checkY && dragDirectionY && target.axis !== AXIS_X;
testPriority = target.priority || 0;
// Ignore this item if it's x-axis and y-axis priority is lower than
// the currently matching item's.
if ((!testAxisX || testPriority < xPriority) && (!testAxisY || testPriority < yPriority)) {
continue;
}
testElement = getScrollElement(target.element || target);
testMaxScrollX = testAxisX ? getScrollLeftMax(testElement) : -1;
testMaxScrollY = testAxisY ? getScrollTopMax(testElement) : -1;
// Ignore this item if there is no possibility to scroll.
if (!testMaxScrollX && !testMaxScrollY) continue;
testRect = getContentRect(testElement, testRect);
testScore = getIntersectionScore(itemRect, testRect);
// Ignore this item if it's not overlapping at all with the dragged item.
if (testScore <= 0) continue;
// Test x-axis.
if (
testAxisX &&
testPriority >= xPriority &&
testMaxScrollX > 0 &&
(testPriority > xPriority || testScore > xScore)
) {
testDirection = null;
testThreshold = computeThreshold(
typeof target.threshold === 'number' ? target.threshold : threshold,
safeZone,
itemRect.width,
testRect.width
);
if (dragDirectionX === RIGHT) {
testDistance = testRect.right + testThreshold.offset - itemRect.right;
if (testDistance <= testThreshold.value && getScrollLeft(testElement) < testMaxScrollX) {
testDirection = RIGHT;
}
} else if (dragDirectionX === LEFT) {
testDistance = itemRect.left - (testRect.left - testThreshold.offset);
if (testDistance <= testThreshold.value && getScrollLeft(testElement) > 0) {
testDirection = LEFT;
}
}
if (testDirection !== null) {
xElement = testElement;
xPriority = testPriority;
xThreshold = testThreshold.value;
xScore = testScore;
xDirection = testDirection;
xDistance = testDistance;
xMaxScroll = testMaxScrollX;
}
}
// Test y-axis.
if (
testAxisY &&
testPriority >= yPriority &&
testMaxScrollY > 0 &&
(testPriority > yPriority || testScore > yScore)
) {
testDirection = null;
testThreshold = computeThreshold(
typeof target.threshold === 'number' ? target.threshold : threshold,
safeZone,
itemRect.height,
testRect.height
);
if (dragDirectionY === DOWN) {
testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom;
if (testDistance <= testThreshold.value && getScrollTop(testElement) < testMaxScrollY) {
testDirection = DOWN;
}
} else if (dragDirectionY === UP) {
testDistance = itemRect.top - (testRect.top - testThreshold.offset);
if (testDistance <= testThreshold.value && getScrollTop(testElement) > 0) {
testDirection = UP;
}
}
if (testDirection !== null) {
yElement = testElement;
yPriority = testPriority;
yThreshold = testThreshold.value;
yScore = testScore;
yDirection = testDirection;
yDistance = testDistance;
yMaxScroll = testMaxScrollY;
}
}
}
// Request or cancel x-axis scroll.
if (checkX) {
if (xElement) {
this._requestItemScroll(
item,
AXIS_X,
xElement,
xDirection,
xThreshold,
xDistance,
xMaxScroll
);
} else {
this._cancelItemScroll(item, AXIS_X);
}
}
// Request or cancel y-axis scroll.
if (checkY) {
if (yElement) {
this._requestItemScroll(
item,
AXIS_Y,
yElement,
yDirection,
yThreshold,
yDistance,
yMaxScroll
);
} else {
this._cancelItemScroll(item, AXIS_Y);
}
}
};
AutoScroller.prototype._updateScrollRequest = function (scrollRequest) {
var item = scrollRequest.item;
var settings = getItemAutoScrollSettings(item);
var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets;
var targetCount = (targets && targets.length) || 0;
var threshold = settings.threshold;
var safeZone = settings.safeZone;
var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1);
var testRect = RECT_2;
var target = null;
var testElement = null;
var testIsAxisX = false;
var testScore = null;
var testThreshold = null;
var testDistance = null;
var testScroll = null;
var testMaxScroll = null;
var hasReachedEnd = null;
for (var i = 0; i < targetCount; i++) {
target = targets[i];
// Make sure we have a matching element.
testElement = getScrollElement(target.element || target);
if (testElement !== scrollRequest.element) continue;
// Make sure we have a matching axis.
testIsAxisX = !!(AXIS_X & scrollRequest.direction);
if (testIsAxisX) {
if (target.axis === AXIS_Y) continue;
} else {
if (target.axis === AXIS_X) continue;
}
// Stop scrolling if there is no room to scroll anymore.
testMaxScroll = testIsAxisX ? getScrollLeftMax(testElement) : getScrollTopMax(testElement);
if (testMaxScroll <= 0) {
break;
}
testRect = getContentRect(testElement, testRect);
testScore = getIntersectionScore(itemRect, testRect);
// Stop scrolling if dragged item is not overlapping with the scroll
// element anymore.
if (testScore <= 0) {
break;
}
// Compute threshold and edge offset.
testThreshold = computeThreshold(
typeof target.threshold === 'number' ? target.threshold : threshold,
safeZone,
testIsAxisX ? itemRect.width : itemRect.height,
testIsAxisX ? testRect.width : testRect.height
);
// Compute distance (based on current direction).
if (scrollRequest.direction === LEFT) {
testDistance = itemRect.left - (testRect.left - testThreshold.offset);
} else if (scrollRequest.direction === RIGHT) {
testDistance = testRect.right + testThreshold.offset - itemRect.right;
} else if (scrollRequest.direction === UP) {
testDistance = itemRect.top - (testRect.top - testThreshold.offset);
} else {
testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom;
}
// Stop scrolling if threshold is not exceeded.
if (testDistance > testThreshold.value) {
break;
}
// Stop scrolling if we have reached the end of the scroll value.
testScroll = testIsAxisX ? getScrollLeft(testElement) : getScrollTop(testElement);
hasReachedEnd =
FORWARD & scrollRequest.direction ? testScroll >= testMaxScroll : testScroll <= 0;
if (hasReachedEnd) {
break;
}
// Scrolling can continue, let's update the values.
scrollRequest.maxValue = testMaxScroll;
scrollRequest.threshold = testThreshold.value;
scrollRequest.distance = testDistance;
scrollRequest.isEnding = false;
return true;
}
// Before we end the request, let's see if we need to stop the scrolling
// smoothly or immediately.
if (settings.smoothStop === true && scrollRequest.speed > 0) {
if (hasReachedEnd === null) hasReachedEnd = scrollRequest.hasReachedEnd();
scrollRequest.isEnding = hasReachedEnd ? false : true;
} else {
scrollRequest.isEnding = false;
}
return scrollRequest.isEnding;
};
AutoScroller.prototype._updateRequests = function () {
var items = this._items;
var requestsX = this._requests[AXIS_X];
var requestsY = this._requests[AXIS_Y];
var item, reqX, reqY, checkTime, needsCheck, checkX, checkY;
for (var i = 0; i < items.length; i++) {
item = items[i];
checkTime = this._requestOverlapCheck[item._id];
needsCheck = checkTime > 0 && this._tickTime - checkTime > this._overlapCheckInterval;
checkX = true;
reqX = requestsX[item._id];
if (reqX && reqX.isActive) {
checkX = !this._updateScrollRequest(reqX);
if (checkX) {
needsCheck = true;
this._cancelItemScroll(item, AXIS_X);
}
}
checkY = true;
reqY = requestsY[item._id];
if (reqY && reqY.isActive) {
checkY = !this._updateScrollRequest(reqY);
if (checkY) {
needsCheck = true;
this._cancelItemScroll(item, AXIS_Y);
}
}
if (needsCheck) {
this._requestOverlapCheck[item._id] = 0;
this._checkItemOverlap(item, checkX, checkY);
}
}
};
AutoScroller.prototype._requestAction = function (request, axis) {
var actions = this._actions;
var isAxisX = axis === AXIS_X;
var action = null;
for (var i = 0; i < actions.length; i++) {
action = actions[i];
// If the action's request does not match the request's -> skip.
if (request.element !== action.element) {
action = null;
continue;
}
// If the request and action share the same element, but the request slot
// for the requested axis is already reserved let's ignore and cancel this
// request.
if (isAxisX ? action.requestX : action.requestY) {
this._cancelItemScroll(request.item, axis);
return;
}
// Seems like we have found our action, let's break the loop.
break;
}
if (!action) action = this._actionPool.pick();
action.element = request.element;
action.addRequest(request);
request.tick(this._tickDeltaTime);
actions.push(action);
};
AutoScroller.prototype._updateActions = function () {
var items = this._items;
var requests = this._requests;
var actions = this._actions;
var itemId;
var reqX;
var reqY;
var i;
// Generate actions.
for (i = 0; i < items.length; i++) {
itemId = items[i]._id;
reqX = requests[AXIS_X][itemId];
reqY = requests[AXIS_Y][itemId];
if (reqX) this._requestAction(reqX, AXIS_X);
if (reqY) this._requestAction(reqY, AXIS_Y);
}
// Compute actions' scroll values.
for (i = 0; i < actions.length; i++) {
actions[i].computeScrollValues();
}
};
AutoScroller.prototype._applyActions = function () {
var actions = this._actions;
var items = this._items;
var i;
// No actions -> no scrolling.
if (!actions.length) return;
// Scroll all the required elements.
for (i = 0; i < actions.length; i++) {
actions[i].scroll();
this._actionPool.release(actions[i]);
}
// Reset actions.
actions.length = 0;
// Sync the item position immediately after all the auto-scrolling business is
// finished. Without this procedure the items will jitter during auto-scroll
// (in some cases at least) since the drag scroll handler is async (bound to
// raf tick). Note that this procedure should not emit any dragScroll events,
// because otherwise they would be emitted twice for the same event.
for (i = 0; i < items.length; i++) prepareItemScrollSync(items[i]);
for (i = 0; i < items.length; i++) applyItemScrollSync(items[i]);
};
AutoScroller.prototype._updateDragDirection = function (item) {
var dragPositions = this._dragPositions[item._id];
var dragDirections = this._dragDirections[item._id];
var x1 = item._drag._left;
var y1 = item._drag._top;
if (dragPositions.length) {
var x2 = dragPositions[0];
var y2 = dragPositions[1];
dragDirections[0] = x1 > x2 ? RIGHT : x1 < x2 ? LEFT : dragDirections[0] || 0;
dragDirections[1] = y1 > y2 ? DOWN : y1 < y2 ? UP : dragDirections[1] || 0;
}
dragPositions[0] = x1;
dragPositions[1] = y1;
};
AutoScroller.prototype.addItem = function (item) {
if (this._isDestroyed) return;
var index = this._items.indexOf(item);
if (index === -1) {
this._items.push(item);
this._requestOverlapCheck[item._id] = this._tickTime;
this._dragDirections[item._id] = [0, 0];
this._dragPositions[item._id] = [];
if (!this._isTicking) this._startTicking();
}
};
AutoScroller.prototype.updateItem = function (item) {
if (this._isDestroyed) return;
// Make sure the item still exists in the auto-scroller.
if (!this._dragDirections[item._id]) return;
this._updateDragDirection(item);
if (!this._requestOverlapCheck[item._id]) {
this._requestOverlapCheck[item._id] = this._tickTime;
}
};
AutoScroller.prototype.removeItem = function (item) {
if (this._isDestroyed) return;
var index = this._items.indexOf(item);
if (index === -1) return;
var itemId = item._id;
var reqX = this._requests[AXIS_X][itemId];
if (reqX) {
this._cancelItemScroll(item, AXIS_X);
delete this._requests[AXIS_X][itemId];
}
var reqY = this._requests[AXIS_Y][itemId];
if (reqY) {
this._cancelItemScroll(item, AXIS_Y);
delete this._requests[AXIS_Y][itemId];
}
delete this._requestOverlapCheck[itemId];
delete this._dragPositions[itemId];
delete this._dragDirections[itemId];
this._items.splice(index, 1);
if (this._isTicking && !this._items.length) {
this._stopTicking();
}
};
AutoScroller.prototype.isItemScrollingX = function (item) {
var reqX = this._requests[AXIS_X][item._id];
return !!(reqX && reqX.isActive);
};
AutoScroller.prototype.isItemScrollingY = function (item) {
var reqY = this._requests[AXIS_Y][item._id];
return !!(reqY && reqY.isActive);
};
AutoScroller.prototype.isItemScrolling = function (item) {
return this.isItemScrollingX(item) || this.isItemScrollingY(item);
};
AutoScroller.prototype.destroy = function () {
if (this._isDestroyed) return;
var items = this._items.slice(0);
for (var i = 0; i < items.length; i++) {
this.removeItem(items[i]);
}
this._actions.length = 0;
this._requestPool.reset();
this._actionPool.reset();
this._isDestroyed = true;
};

View File

@ -0,0 +1,19 @@
Copyright (c) 2019, Niklas Rämö
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,26 @@
/**
* Muuri AutoScroller
* Copyright (c) 2019-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md
*/
export default function Pool(createItem, releaseItem) {
this.pool = [];
this.createItem = createItem;
this.releaseItem = releaseItem;
}
Pool.prototype.pick = function () {
return this.pool.pop() || this.createItem();
};
Pool.prototype.release = function (item) {
this.releaseItem(item);
if (this.pool.indexOf(item) !== -1) return;
this.pool.push(item);
};
Pool.prototype.reset = function () {
this.pool.length = 0;
};

View File

@ -0,0 +1,66 @@
/**
* Muuri AutoScroller
* Copyright (c) 2019-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md
*/
import { getScrollLeft, getScrollTop } from './utils';
import { AXIS_X } from './constants';
export default function ScrollAction() {
this.element = null;
this.requestX = null;
this.requestY = null;
this.scrollLeft = 0;
this.scrollTop = 0;
}
ScrollAction.prototype.reset = function () {
if (this.requestX) this.requestX.action = null;
if (this.requestY) this.requestY.action = null;
this.element = null;
this.requestX = null;
this.requestY = null;
this.scrollLeft = 0;
this.scrollTop = 0;
};
ScrollAction.prototype.addRequest = function (request) {
if (AXIS_X & request.direction) {
this.removeRequest(this.requestX);
this.requestX = request;
} else {
this.removeRequest(this.requestY);
this.requestY = request;
}
request.action = this;
};
ScrollAction.prototype.removeRequest = function (request) {
if (!request) return;
if (this.requestX === request) {
this.requestX = null;
request.action = null;
} else if (this.requestY === request) {
this.requestY = null;
request.action = null;
}
};
ScrollAction.prototype.computeScrollValues = function () {
this.scrollLeft = this.requestX ? this.requestX.value : getScrollLeft(this.element);
this.scrollTop = this.requestY ? this.requestY.value : getScrollTop(this.element);
};
ScrollAction.prototype.scroll = function () {
var element = this.element;
if (!element) return;
if (element.scrollTo) {
element.scrollTo(this.scrollLeft, this.scrollTop);
} else {
element.scrollLeft = this.scrollLeft;
element.scrollTop = this.scrollTop;
}
};

View File

@ -0,0 +1,108 @@
/**
* Muuri AutoScroller
* Copyright (c) 2019-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md
*/
import isFunction from '../utils/isFunction';
import { AXIS_X, FORWARD } from './constants';
import { getScrollLeft, getScrollTop, getItemAutoScrollSettings } from './utils';
export default function ScrollRequest() {
this.reset();
}
ScrollRequest.prototype.reset = function () {
if (this.isActive) this.onStop();
this.item = null;
this.element = null;
this.isActive = false;
this.isEnding = false;
this.direction = null;
this.value = null;
this.maxValue = 0;
this.threshold = 0;
this.distance = 0;
this.speed = 0;
this.duration = 0;
this.action = null;
};
ScrollRequest.prototype.hasReachedEnd = function () {
return FORWARD & this.direction ? this.value >= this.maxValue : this.value <= 0;
};
ScrollRequest.prototype.computeCurrentScrollValue = function () {
if (this.value === null) {
return AXIS_X & this.direction ? getScrollLeft(this.element) : getScrollTop(this.element);
}
return Math.max(0, Math.min(this.value, this.maxValue));
};
ScrollRequest.prototype.computeNextScrollValue = function (deltaTime) {
var delta = this.speed * (deltaTime / 1000);
var nextValue = FORWARD & this.direction ? this.value + delta : this.value - delta;
return Math.max(0, Math.min(nextValue, this.maxValue));
};
ScrollRequest.prototype.computeSpeed = (function () {
var data = {
direction: null,
threshold: 0,
distance: 0,
value: 0,
maxValue: 0,
deltaTime: 0,
duration: 0,
isEnding: false,
};
return function (deltaTime) {
var item = this.item;
var speed = getItemAutoScrollSettings(item).speed;
if (isFunction(speed)) {
data.direction = this.direction;
data.threshold = this.threshold;
data.distance = this.distance;
data.value = this.value;
data.maxValue = this.maxValue;
data.duration = this.duration;
data.speed = this.speed;
data.deltaTime = deltaTime;
data.isEnding = this.isEnding;
return speed(item, this.element, data);
} else {
return speed;
}
};
})();
ScrollRequest.prototype.tick = function (deltaTime) {
if (!this.isActive) {
this.isActive = true;
this.onStart();
}
this.value = this.computeCurrentScrollValue();
this.speed = this.computeSpeed(deltaTime);
this.value = this.computeNextScrollValue(deltaTime);
this.duration += deltaTime;
return this.value;
};
ScrollRequest.prototype.onStart = function () {
var item = this.item;
var onStart = getItemAutoScrollSettings(item).onStart;
if (isFunction(onStart)) onStart(item, this.element, this.direction);
};
ScrollRequest.prototype.onStop = function () {
var item = this.item;
var onStop = getItemAutoScrollSettings(item).onStop;
if (isFunction(onStop)) onStop(item, this.element, this.direction);
// Manually nudge sort to happen. There's a good chance that the item is still
// after the scroll stops which means that the next sort will be triggered
// only after the item is moved or it's parent scrolled.
if (item._drag) item._drag.sort();
};

View File

@ -0,0 +1,15 @@
/**
* Muuri AutoScroller
* Copyright (c) 2019-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md
*/
export var AXIS_X = 1;
export var AXIS_Y = 2;
export var FORWARD = 4;
export var BACKWARD = 8;
export var LEFT = AXIS_X | BACKWARD;
export var RIGHT = AXIS_X | FORWARD;
export var UP = AXIS_Y | BACKWARD;
export var DOWN = AXIS_Y | FORWARD;

View File

@ -0,0 +1,139 @@
/**
* Muuri AutoScroller
* Copyright (c) 2019-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md
*/
import getStyleAsFloat from '../utils/getStyleAsFloat';
var DOC_ELEM = document.documentElement;
var BODY = document.body;
var THRESHOLD_DATA = { value: 0, offset: 0 };
/**
* @param {HTMLElement|Window} element
* @returns {HTMLElement|Window}
*/
export function getScrollElement(element) {
if (element === window || element === DOC_ELEM || element === BODY) {
return window;
} else {
return element;
}
}
/**
* @param {HTMLElement|Window} element
* @returns {Number}
*/
export function getScrollLeft(element) {
return element === window ? element.pageXOffset : element.scrollLeft;
}
/**
* @param {HTMLElement|Window} element
* @returns {Number}
*/
export function getScrollTop(element) {
return element === window ? element.pageYOffset : element.scrollTop;
}
/**
* @param {HTMLElement|Window} element
* @returns {Number}
*/
export function getScrollLeftMax(element) {
if (element === window) {
return DOC_ELEM.scrollWidth - DOC_ELEM.clientWidth;
} else {
return element.scrollWidth - element.clientWidth;
}
}
/**
* @param {HTMLElement|Window} element
* @returns {Number}
*/
export function getScrollTopMax(element) {
if (element === window) {
return DOC_ELEM.scrollHeight - DOC_ELEM.clientHeight;
} else {
return element.scrollHeight - element.clientHeight;
}
}
/**
* Get window's or element's client rectangle data relative to the element's
* content dimensions (includes inner size + padding, excludes scrollbars,
* borders and margins).
*
* @param {HTMLElement|Window} element
* @returns {Rectangle}
*/
export function getContentRect(element, result) {
result = result || {};
if (element === window) {
result.width = DOC_ELEM.clientWidth;
result.height = DOC_ELEM.clientHeight;
result.left = 0;
result.right = result.width;
result.top = 0;
result.bottom = result.height;
} else {
var bcr = element.getBoundingClientRect();
var borderLeft = element.clientLeft || getStyleAsFloat(element, 'border-left-width');
var borderTop = element.clientTop || getStyleAsFloat(element, 'border-top-width');
result.width = element.clientWidth;
result.height = element.clientHeight;
result.left = bcr.left + borderLeft;
result.right = result.left + result.width;
result.top = bcr.top + borderTop;
result.bottom = result.top + result.height;
}
return result;
}
/**
* @param {Item} item
* @returns {Object}
*/
export function getItemAutoScrollSettings(item) {
return item._drag._getGrid()._settings.dragAutoScroll;
}
/**
* @param {Item} item
*/
export function prepareItemScrollSync(item) {
if (!item._drag) return;
item._drag._prepareScroll();
}
/**
* @param {Item} item
*/
export function applyItemScrollSync(item) {
if (!item._drag || !item._isActive) return;
var drag = item._drag;
drag._scrollDiffX = drag._scrollDiffY = 0;
item._setTranslate(drag._left, drag._top);
}
/**
* Compute threshold value and edge offset.
*
* @param {Number} threshold
* @param {Number} safeZone
* @param {Number} itemSize
* @param {Number} targetSize
* @returns {Object}
*/
export function computeThreshold(threshold, safeZone, itemSize, targetSize) {
THRESHOLD_DATA.value = Math.min(targetSize / 2, threshold);
THRESHOLD_DATA.offset =
Math.max(0, itemSize + THRESHOLD_DATA.value * 2 + targetSize * safeZone - targetSize) / 2;
return THRESHOLD_DATA;
}

View File

@ -0,0 +1,575 @@
/**
* Muuri Dragger
* Copyright (c) 2018-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/Dragger/LICENSE.md
*/
import { HAS_TOUCH_EVENTS, HAS_POINTER_EVENTS, HAS_MS_POINTER_EVENTS } from '../constants';
import Emitter from '../Emitter/Emitter';
import EdgeHack from './EdgeHack';
import getPrefixedPropName from '../utils/getPrefixedPropName';
import hasPassiveEvents from '../utils/hasPassiveEvents';
var ua = window.navigator.userAgent.toLowerCase();
var isEdge = ua.indexOf('edge') > -1;
var isIE = ua.indexOf('trident') > -1;
var isFirefox = ua.indexOf('firefox') > -1;
var isAndroid = ua.indexOf('android') > -1;
var listenerOptions = hasPassiveEvents() ? { passive: true } : false;
var taProp = 'touchAction';
var taPropPrefixed = getPrefixedPropName(document.documentElement.style, taProp);
var taDefaultValue = 'auto';
/**
* Creates a new Dragger instance for an element.
*
* @public
* @class
* @param {HTMLElement} element
* @param {Object} [cssProps]
*/
function Dragger(element, cssProps) {
this._element = element;
this._emitter = new Emitter();
this._isDestroyed = false;
this._cssProps = {};
this._touchAction = '';
this._isActive = false;
this._pointerId = null;
this._startTime = 0;
this._startX = 0;
this._startY = 0;
this._currentX = 0;
this._currentY = 0;
this._onStart = this._onStart.bind(this);
this._onMove = this._onMove.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onEnd = this._onEnd.bind(this);
// Can't believe had to build a freaking class for a hack!
this._edgeHack = null;
if ((isEdge || isIE) && (HAS_POINTER_EVENTS || HAS_MS_POINTER_EVENTS)) {
this._edgeHack = new EdgeHack(this);
}
// Apply initial CSS props.
this.setCssProps(cssProps);
// If touch action was not provided with initial CSS props let's assume it's
// auto.
if (!this._touchAction) {
this.setTouchAction(taDefaultValue);
}
// Prevent native link/image dragging for the item and it's children.
element.addEventListener('dragstart', Dragger._preventDefault, false);
// Listen to start event.
element.addEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions);
}
/**
* Protected properties
* ********************
*/
Dragger._pointerEvents = {
start: 'pointerdown',
move: 'pointermove',
cancel: 'pointercancel',
end: 'pointerup',
};
Dragger._msPointerEvents = {
start: 'MSPointerDown',
move: 'MSPointerMove',
cancel: 'MSPointerCancel',
end: 'MSPointerUp',
};
Dragger._touchEvents = {
start: 'touchstart',
move: 'touchmove',
cancel: 'touchcancel',
end: 'touchend',
};
Dragger._mouseEvents = {
start: 'mousedown',
move: 'mousemove',
cancel: '',
end: 'mouseup',
};
Dragger._inputEvents = (function () {
if (HAS_TOUCH_EVENTS) return Dragger._touchEvents;
if (HAS_POINTER_EVENTS) return Dragger._pointerEvents;
if (HAS_MS_POINTER_EVENTS) return Dragger._msPointerEvents;
return Dragger._mouseEvents;
})();
Dragger._emitter = new Emitter();
Dragger._emitterEvents = {
start: 'start',
move: 'move',
end: 'end',
cancel: 'cancel',
};
Dragger._activeInstances = [];
/**
* Protected static methods
* ************************
*/
Dragger._preventDefault = function (e) {
if (e.preventDefault && e.cancelable !== false) e.preventDefault();
};
Dragger._activateInstance = function (instance) {
var index = Dragger._activeInstances.indexOf(instance);
if (index > -1) return;
Dragger._activeInstances.push(instance);
Dragger._emitter.on(Dragger._emitterEvents.move, instance._onMove);
Dragger._emitter.on(Dragger._emitterEvents.cancel, instance._onCancel);
Dragger._emitter.on(Dragger._emitterEvents.end, instance._onEnd);
if (Dragger._activeInstances.length === 1) {
Dragger._bindListeners();
}
};
Dragger._deactivateInstance = function (instance) {
var index = Dragger._activeInstances.indexOf(instance);
if (index === -1) return;
Dragger._activeInstances.splice(index, 1);
Dragger._emitter.off(Dragger._emitterEvents.move, instance._onMove);
Dragger._emitter.off(Dragger._emitterEvents.cancel, instance._onCancel);
Dragger._emitter.off(Dragger._emitterEvents.end, instance._onEnd);
if (!Dragger._activeInstances.length) {
Dragger._unbindListeners();
}
};
Dragger._bindListeners = function () {
window.addEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions);
window.addEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions);
if (Dragger._inputEvents.cancel) {
window.addEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions);
}
};
Dragger._unbindListeners = function () {
window.removeEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions);
window.removeEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions);
if (Dragger._inputEvents.cancel) {
window.removeEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions);
}
};
Dragger._getEventPointerId = function (event) {
// If we have pointer id available let's use it.
if (typeof event.pointerId === 'number') {
return event.pointerId;
}
// For touch events let's get the first changed touch's identifier.
if (event.changedTouches) {
return event.changedTouches[0] ? event.changedTouches[0].identifier : null;
}
// For mouse/other events let's provide a static id.
return 1;
};
Dragger._getTouchById = function (event, id) {
// If we have a pointer event return the whole event if there's a match, and
// null otherwise.
if (typeof event.pointerId === 'number') {
return event.pointerId === id ? event : null;
}
// For touch events let's check if there's a changed touch object that matches
// the pointerId in which case return the touch object.
if (event.changedTouches) {
for (var i = 0; i < event.changedTouches.length; i++) {
if (event.changedTouches[i].identifier === id) {
return event.changedTouches[i];
}
}
return null;
}
// For mouse/other events let's assume there's only one pointer and just
// return the event.
return event;
};
Dragger._onMove = function (e) {
Dragger._emitter.emit(Dragger._emitterEvents.move, e);
};
Dragger._onCancel = function (e) {
Dragger._emitter.emit(Dragger._emitterEvents.cancel, e);
};
Dragger._onEnd = function (e) {
Dragger._emitter.emit(Dragger._emitterEvents.end, e);
};
/**
* Private prototype methods
* *************************
*/
/**
* Reset current drag operation (if any).
*
* @private
*/
Dragger.prototype._reset = function () {
this._pointerId = null;
this._startTime = 0;
this._startX = 0;
this._startY = 0;
this._currentX = 0;
this._currentY = 0;
this._isActive = false;
Dragger._deactivateInstance(this);
};
/**
* Create a custom dragger event from a raw event.
*
* @private
* @param {String} type
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
* @returns {Object}
*/
Dragger.prototype._createEvent = function (type, e) {
var touch = this._getTrackedTouch(e);
return {
// Hammer.js compatibility interface.
type: type,
srcEvent: e,
distance: this.getDistance(),
deltaX: this.getDeltaX(),
deltaY: this.getDeltaY(),
deltaTime: type === Dragger._emitterEvents.start ? 0 : this.getDeltaTime(),
isFirst: type === Dragger._emitterEvents.start,
isFinal: type === Dragger._emitterEvents.end || type === Dragger._emitterEvents.cancel,
pointerType: e.pointerType || (e.touches ? 'touch' : 'mouse'),
// Partial Touch API interface.
identifier: this._pointerId,
screenX: touch.screenX,
screenY: touch.screenY,
clientX: touch.clientX,
clientY: touch.clientY,
pageX: touch.pageX,
pageY: touch.pageY,
target: touch.target,
};
};
/**
* Emit a raw event as dragger event internally.
*
* @private
* @param {String} type
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._emit = function (type, e) {
this._emitter.emit(type, this._createEvent(type, e));
};
/**
* If the provided event is a PointerEvent this method will return it if it has
* the same pointerId as the instance. If the provided event is a TouchEvent
* this method will try to look for a Touch instance in the changedTouches that
* has an identifier matching this instance's pointerId. If the provided event
* is a MouseEvent (or just any other event than PointerEvent or TouchEvent)
* it will be returned immediately.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
* @returns {?(Touch|PointerEvent|MouseEvent)}
*/
Dragger.prototype._getTrackedTouch = function (e) {
if (this._pointerId === null) return null;
return Dragger._getTouchById(e, this._pointerId);
};
/**
* Handler for start event.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onStart = function (e) {
if (this._isDestroyed) return;
// If pointer id is already assigned let's return early.
if (this._pointerId !== null) return;
// Get (and set) pointer id.
this._pointerId = Dragger._getEventPointerId(e);
if (this._pointerId === null) return;
// Setup initial data and emit start event.
var touch = this._getTrackedTouch(e);
this._startX = this._currentX = touch.clientX;
this._startY = this._currentY = touch.clientY;
this._startTime = Date.now();
this._isActive = true;
this._emit(Dragger._emitterEvents.start, e);
// If the drag procedure was not reset within the start procedure let's
// activate the instance (start listening to move/cancel/end events).
if (this._isActive) {
Dragger._activateInstance(this);
}
};
/**
* Handler for move event.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onMove = function (e) {
var touch = this._getTrackedTouch(e);
if (!touch) return;
this._currentX = touch.clientX;
this._currentY = touch.clientY;
this._emit(Dragger._emitterEvents.move, e);
};
/**
* Handler for cancel event.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onCancel = function (e) {
if (!this._getTrackedTouch(e)) return;
this._emit(Dragger._emitterEvents.cancel, e);
this._reset();
};
/**
* Handler for end event.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onEnd = function (e) {
if (!this._getTrackedTouch(e)) return;
this._emit(Dragger._emitterEvents.end, e);
this._reset();
};
/**
* Public prototype methods
* ************************
*/
/**
* Check if the element is being dragged at the moment.
*
* @public
* @returns {Boolean}
*/
Dragger.prototype.isActive = function () {
return this._isActive;
};
/**
* Set element's touch-action CSS property.
*
* @public
* @param {String} value
*/
Dragger.prototype.setTouchAction = function (value) {
// Store unmodified touch action value (we trust user input here).
this._touchAction = value;
// Set touch-action style.
if (taPropPrefixed) {
this._cssProps[taPropPrefixed] = '';
this._element.style[taPropPrefixed] = value;
}
// If we have an unsupported touch-action value let's add a special listener
// that prevents default action on touch start event. A dirty hack, but best
// we can do for now. The other options would be to somehow polyfill the
// unsupported touch action behavior with custom heuristics which sounds like
// a can of worms. We do a special exception here for Firefox Android which's
// touch-action does not work properly if the dragged element is moved in the
// the DOM tree on touchstart.
if (HAS_TOUCH_EVENTS) {
this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true);
if (this._element.style[taPropPrefixed] !== value || (isFirefox && isAndroid)) {
this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true);
}
}
};
/**
* Update element's CSS properties. Accepts an object with camel cased style
* props with value pairs as it's first argument.
*
* @public
* @param {Object} [newProps]
*/
Dragger.prototype.setCssProps = function (newProps) {
if (!newProps) return;
var currentProps = this._cssProps;
var element = this._element;
var prop;
var prefixedProp;
// Reset current props.
for (prop in currentProps) {
element.style[prop] = currentProps[prop];
delete currentProps[prop];
}
// Set new props.
for (prop in newProps) {
// Make sure we have a value for the prop.
if (!newProps[prop]) continue;
// Special handling for touch-action.
if (prop === taProp) {
this.setTouchAction(newProps[prop]);
continue;
}
// Get prefixed prop and skip if it does not exist.
prefixedProp = getPrefixedPropName(element.style, prop);
if (!prefixedProp) continue;
// Store the prop and add the style.
currentProps[prefixedProp] = '';
element.style[prefixedProp] = newProps[prop];
}
};
/**
* How much the pointer has moved on x-axis from start position, in pixels.
* Positive value indicates movement from left to right.
*
* @public
* @returns {Number}
*/
Dragger.prototype.getDeltaX = function () {
return this._currentX - this._startX;
};
/**
* How much the pointer has moved on y-axis from start position, in pixels.
* Positive value indicates movement from top to bottom.
*
* @public
* @returns {Number}
*/
Dragger.prototype.getDeltaY = function () {
return this._currentY - this._startY;
};
/**
* How far (in pixels) has pointer moved from start position.
*
* @public
* @returns {Number}
*/
Dragger.prototype.getDistance = function () {
var x = this.getDeltaX();
var y = this.getDeltaY();
return Math.sqrt(x * x + y * y);
};
/**
* How long has pointer been dragged.
*
* @public
* @returns {Number}
*/
Dragger.prototype.getDeltaTime = function () {
return this._startTime ? Date.now() - this._startTime : 0;
};
/**
* Bind drag event listeners.
*
* @public
* @param {String} eventName
* - 'start', 'move', 'cancel' or 'end'.
* @param {Function} listener
*/
Dragger.prototype.on = function (eventName, listener) {
this._emitter.on(eventName, listener);
};
/**
* Unbind drag event listeners.
*
* @public
* @param {String} eventName
* - 'start', 'move', 'cancel' or 'end'.
* @param {Function} listener
*/
Dragger.prototype.off = function (eventName, listener) {
this._emitter.off(eventName, listener);
};
/**
* Destroy the instance and unbind all drag event listeners.
*
* @public
*/
Dragger.prototype.destroy = function () {
if (this._isDestroyed) return;
var element = this._element;
if (this._edgeHack) this._edgeHack.destroy();
// Reset data and deactivate the instance.
this._reset();
// Destroy emitter.
this._emitter.destroy();
// Unbind event handlers.
element.removeEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions);
element.removeEventListener('dragstart', Dragger._preventDefault, false);
element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true);
// Reset styles.
for (var prop in this._cssProps) {
element.style[prop] = this._cssProps[prop];
delete this._cssProps[prop];
}
// Reset data.
this._element = null;
// Mark as destroyed.
this._isDestroyed = true;
};
export default Dragger;

View File

@ -0,0 +1,119 @@
/**
* Muuri Dragger
* Copyright (c) 2018-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/Dragger/LICENSE.md
*/
import { HAS_POINTER_EVENTS, HAS_MS_POINTER_EVENTS } from '../constants';
var pointerout = HAS_POINTER_EVENTS ? 'pointerout' : HAS_MS_POINTER_EVENTS ? 'MSPointerOut' : '';
var waitDuration = 100;
/**
* If you happen to use Edge or IE on a touch capable device there is a
* a specific case where pointercancel and pointerend events are never emitted,
* even though one them should always be emitted when you release your finger
* from the screen. The bug appears specifically when Muuri shifts the dragged
* element's position in the DOM after pointerdown event, IE and Edge don't like
* that behaviour and quite often forget to emit the pointerend/pointercancel
* event. But, they do emit pointerout event so we utilize that here.
* Specifically, if there has been no pointermove event within 100 milliseconds
* since the last pointerout event we force cancel the drag operation. This hack
* works surprisingly well 99% of the time. There is that 1% chance there still
* that dragged items get stuck but it is what it is.
*
* @class
* @param {Dragger} dragger
*/
function EdgeHack(dragger) {
if (!pointerout) return;
this._dragger = dragger;
this._timeout = null;
this._outEvent = null;
this._isActive = false;
this._addBehaviour = this._addBehaviour.bind(this);
this._removeBehaviour = this._removeBehaviour.bind(this);
this._onTimeout = this._onTimeout.bind(this);
this._resetData = this._resetData.bind(this);
this._onStart = this._onStart.bind(this);
this._onOut = this._onOut.bind(this);
this._dragger.on('start', this._onStart);
}
/**
* @private
*/
EdgeHack.prototype._addBehaviour = function () {
if (this._isActive) return;
this._isActive = true;
this._dragger.on('move', this._resetData);
this._dragger.on('cancel', this._removeBehaviour);
this._dragger.on('end', this._removeBehaviour);
window.addEventListener(pointerout, this._onOut);
};
/**
* @private
*/
EdgeHack.prototype._removeBehaviour = function () {
if (!this._isActive) return;
this._dragger.off('move', this._resetData);
this._dragger.off('cancel', this._removeBehaviour);
this._dragger.off('end', this._removeBehaviour);
window.removeEventListener(pointerout, this._onOut);
this._resetData();
this._isActive = false;
};
/**
* @private
*/
EdgeHack.prototype._resetData = function () {
window.clearTimeout(this._timeout);
this._timeout = null;
this._outEvent = null;
};
/**
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
EdgeHack.prototype._onStart = function (e) {
if (e.pointerType === 'mouse') return;
this._addBehaviour();
};
/**
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
EdgeHack.prototype._onOut = function (e) {
if (!this._dragger._getTrackedTouch(e)) return;
this._resetData();
this._outEvent = e;
this._timeout = window.setTimeout(this._onTimeout, waitDuration);
};
/**
* @private
*/
EdgeHack.prototype._onTimeout = function () {
var e = this._outEvent;
this._resetData();
if (this._dragger.isActive()) this._dragger._onCancel(e);
};
/**
* @public
*/
EdgeHack.prototype.destroy = function () {
if (!pointerout) return;
this._dragger.off('start', this._onStart);
this._removeBehaviour();
};
export default EdgeHack;

View File

@ -0,0 +1,19 @@
Copyright (c) 2018, Niklas Rämö
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,208 @@
/**
* Muuri Emitter
* Copyright (c) 2018-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/Emitter/LICENSE.md
*/
/**
* Event emitter constructor.
*
* @class
*/
function Emitter() {
this._events = {};
this._queue = [];
this._counter = 0;
this._clearOnEmit = false;
}
/**
* Public prototype methods
* ************************
*/
/**
* Bind an event listener.
*
* @public
* @param {String} event
* @param {Function} listener
* @returns {Emitter}
*/
Emitter.prototype.on = function (event, listener) {
if (!this._events || !event || !listener) return this;
// Get listeners queue and create it if it does not exist.
var listeners = this._events[event];
if (!listeners) listeners = this._events[event] = [];
// Add the listener to the queue.
listeners.push(listener);
return this;
};
/**
* Unbind all event listeners that match the provided listener function.
*
* @public
* @param {String} event
* @param {Function} listener
* @returns {Emitter}
*/
Emitter.prototype.off = function (event, listener) {
if (!this._events || !event || !listener) return this;
// Get listeners and return immediately if none is found.
var listeners = this._events[event];
if (!listeners || !listeners.length) return this;
// Remove all matching listeners.
var index;
while ((index = listeners.indexOf(listener)) !== -1) {
listeners.splice(index, 1);
}
return this;
};
/**
* Unbind all listeners of the provided event.
*
* @public
* @param {String} event
* @returns {Emitter}
*/
Emitter.prototype.clear = function (event) {
if (!this._events || !event) return this;
var listeners = this._events[event];
if (listeners) {
listeners.length = 0;
delete this._events[event];
}
return this;
};
/**
* Emit all listeners in a specified event with the provided arguments.
*
* @public
* @param {String} event
* @param {...*} [args]
* @returns {Emitter}
*/
Emitter.prototype.emit = function (event) {
if (!this._events || !event) {
this._clearOnEmit = false;
return this;
}
// Get event listeners and quit early if there's no listeners.
var listeners = this._events[event];
if (!listeners || !listeners.length) {
this._clearOnEmit = false;
return this;
}
var queue = this._queue;
var startIndex = queue.length;
var argsLength = arguments.length - 1;
var args;
// If we have more than 3 arguments let's put the arguments in an array and
// apply it to the listeners.
if (argsLength > 3) {
args = [];
args.push.apply(args, arguments);
args.shift();
}
// Add the current listeners to the callback queue before we process them.
// This is necessary to guarantee that all of the listeners are called in
// correct order even if new event listeners are removed/added during
// processing and/or events are emitted during processing.
queue.push.apply(queue, listeners);
// Reset the event's listeners if need be.
if (this._clearOnEmit) {
listeners.length = 0;
this._clearOnEmit = false;
}
// Increment queue counter. This is needed for the scenarios where emit is
// triggered while the queue is already processing. We need to keep track of
// how many "queue processors" there are active so that we can safely reset
// the queue in the end when the last queue processor is finished.
++this._counter;
// Process the queue (the specific part of it for this emit).
var i = startIndex;
var endIndex = queue.length;
for (; i < endIndex; i++) {
// prettier-ignore
argsLength === 0 ? queue[i]() :
argsLength === 1 ? queue[i](arguments[1]) :
argsLength === 2 ? queue[i](arguments[1], arguments[2]) :
argsLength === 3 ? queue[i](arguments[1], arguments[2], arguments[3]) :
queue[i].apply(null, args);
// Stop processing if the emitter is destroyed.
if (!this._events) return this;
}
// Decrement queue process counter.
--this._counter;
// Reset the queue if there are no more queue processes running.
if (!this._counter) queue.length = 0;
return this;
};
/**
* Emit all listeners in a specified event with the provided arguments and
* remove the event's listeners just before calling the them. This method allows
* the emitter to serve as a queue where all listeners are called only once.
*
* @public
* @param {String} event
* @param {...*} [args]
* @returns {Emitter}
*/
Emitter.prototype.burst = function () {
if (!this._events) return this;
this._clearOnEmit = true;
this.emit.apply(this, arguments);
return this;
};
/**
* Check how many listeners there are for a specific event.
*
* @public
* @param {String} event
* @returns {Boolean}
*/
Emitter.prototype.countListeners = function (event) {
if (!this._events) return 0;
var listeners = this._events[event];
return listeners ? listeners.length : 0;
};
/**
* Destroy emitter instance. Basically just removes all bound listeners.
*
* @public
* @returns {Emitter}
*/
Emitter.prototype.destroy = function () {
if (!this._events) return this;
this._queue.length = this._counter = 0;
this._events = null;
return this;
};
export default Emitter;

View File

@ -0,0 +1,19 @@
Copyright (c) 2018, Niklas Rämö
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1781
app/src/vendor/muuri-src/Grid/Grid.js vendored Normal file

File diff suppressed because it is too large Load Diff

420
app/src/vendor/muuri-src/Item/Item.js vendored Normal file
View File

@ -0,0 +1,420 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import { GRID_INSTANCES, ITEM_ELEMENT_MAP } from '../constants';
import ItemDrag from './ItemDrag';
import ItemDragPlaceholder from './ItemDragPlaceholder';
import ItemDragRelease from './ItemDragRelease';
import ItemLayout from './ItemLayout';
import ItemMigrate from './ItemMigrate';
import ItemVisibility from './ItemVisibility';
import Emitter from '../Emitter/Emitter';
import addClass from '../utils/addClass';
import createUid from '../utils/createUid';
import getStyle from '../utils/getStyle';
import getStyleAsFloat from '../utils/getStyleAsFloat';
import getTranslateString from '../utils/getTranslateString';
import removeClass from '../utils/removeClass';
import transformProp from '../utils/transformProp';
/**
* Creates a new Item instance for a Grid instance.
*
* @class
* @param {Grid} grid
* @param {HTMLElement} element
* @param {Boolean} [isActive]
*/
function Item(grid, element, isActive) {
var settings = grid._settings;
// Store item/element pair to a map (for faster item querying by element).
if (ITEM_ELEMENT_MAP) {
if (ITEM_ELEMENT_MAP.has(element)) {
throw new Error('You can only create one Muuri Item per element!');
} else {
ITEM_ELEMENT_MAP.set(element, this);
}
}
this._id = createUid();
this._gridId = grid._id;
this._element = element;
this._isDestroyed = false;
this._left = 0;
this._top = 0;
this._width = 0;
this._height = 0;
this._marginLeft = 0;
this._marginRight = 0;
this._marginTop = 0;
this._marginBottom = 0;
this._tX = undefined;
this._tY = undefined;
this._sortData = null;
this._emitter = new Emitter();
// If the provided item element is not a direct child of the grid container
// element, append it to the grid container. Note, we are indeed reading the
// DOM here but it's a property that does not cause reflowing.
if (element.parentNode !== grid._element) {
grid._element.appendChild(element);
}
// Set item class.
addClass(element, settings.itemClass);
// If isActive is not defined, let's try to auto-detect it. Note, we are
// indeed reading the DOM here but it's a property that does not cause
// reflowing.
if (typeof isActive !== 'boolean') {
isActive = getStyle(element, 'display') !== 'none';
}
// Set up active state (defines if the item is considered part of the layout
// or not).
this._isActive = isActive;
// Setup visibility handler.
this._visibility = new ItemVisibility(this);
// Set up layout handler.
this._layout = new ItemLayout(this);
// Set up migration handler data.
this._migrate = new ItemMigrate(this);
// Set up drag handler.
this._drag = settings.dragEnabled ? new ItemDrag(this) : null;
// Set up release handler. Note that although this is fully linked to dragging
// this still needs to be always instantiated to handle migration scenarios
// correctly.
this._dragRelease = new ItemDragRelease(this);
// Set up drag placeholder handler. Note that although this is fully linked to
// dragging this still needs to be always instantiated to handle migration
// scenarios correctly.
this._dragPlaceholder = new ItemDragPlaceholder(this);
// Note! You must call the following methods before you start using the
// instance. They are deliberately not called in the end as it would cause
// potentially a massive amount of reflows if multiple items were instantiated
// in a loop.
// this._refreshDimensions();
// this._refreshSortData();
}
/**
* Public prototype methods
* ************************
*/
/**
* Get the instance grid reference.
*
* @public
* @returns {Grid}
*/
Item.prototype.getGrid = function () {
return GRID_INSTANCES[this._gridId];
};
/**
* Get the instance element.
*
* @public
* @returns {HTMLElement}
*/
Item.prototype.getElement = function () {
return this._element;
};
/**
* Get instance element's cached width.
*
* @public
* @returns {Number}
*/
Item.prototype.getWidth = function () {
return this._width;
};
/**
* Get instance element's cached height.
*
* @public
* @returns {Number}
*/
Item.prototype.getHeight = function () {
return this._height;
};
/**
* Get instance element's cached margins.
*
* @public
* @returns {Object}
* - The returned object contains left, right, top and bottom properties
* which indicate the item element's cached margins.
*/
Item.prototype.getMargin = function () {
return {
left: this._marginLeft,
right: this._marginRight,
top: this._marginTop,
bottom: this._marginBottom,
};
};
/**
* Get instance element's cached position.
*
* @public
* @returns {Object}
* - The returned object contains left and top properties which indicate the
* item element's cached position in the grid.
*/
Item.prototype.getPosition = function () {
return {
left: this._left,
top: this._top,
};
};
/**
* Is the item active?
*
* @public
* @returns {Boolean}
*/
Item.prototype.isActive = function () {
return this._isActive;
};
/**
* Is the item visible?
*
* @public
* @returns {Boolean}
*/
Item.prototype.isVisible = function () {
return !!this._visibility && !this._visibility._isHidden;
};
/**
* Is the item being animated to visible?
*
* @public
* @returns {Boolean}
*/
Item.prototype.isShowing = function () {
return !!(this._visibility && this._visibility._isShowing);
};
/**
* Is the item being animated to hidden?
*
* @public
* @returns {Boolean}
*/
Item.prototype.isHiding = function () {
return !!(this._visibility && this._visibility._isHiding);
};
/**
* Is the item positioning?
*
* @public
* @returns {Boolean}
*/
Item.prototype.isPositioning = function () {
return !!(this._layout && this._layout._isActive);
};
/**
* Is the item being dragged (or queued for dragging)?
*
* @public
* @returns {Boolean}
*/
Item.prototype.isDragging = function () {
return !!(this._drag && this._drag._isActive);
};
/**
* Is the item being released?
*
* @public
* @returns {Boolean}
*/
Item.prototype.isReleasing = function () {
return !!(this._dragRelease && this._dragRelease._isActive);
};
/**
* Is the item destroyed?
*
* @public
* @returns {Boolean}
*/
Item.prototype.isDestroyed = function () {
return this._isDestroyed;
};
/**
* Private prototype methods
* *************************
*/
/**
* Recalculate item's dimensions.
*
* @private
* @param {Boolean} [force=false]
*/
Item.prototype._refreshDimensions = function (force) {
if (this._isDestroyed) return;
if (force !== true && this._visibility._isHidden) return;
var element = this._element;
var dragPlaceholder = this._dragPlaceholder;
var rect = element.getBoundingClientRect();
// Calculate width and height.
this._width = rect.width;
this._height = rect.height;
// Calculate margins (ignore negative margins).
this._marginLeft = Math.max(0, getStyleAsFloat(element, 'margin-left'));
this._marginRight = Math.max(0, getStyleAsFloat(element, 'margin-right'));
this._marginTop = Math.max(0, getStyleAsFloat(element, 'margin-top'));
this._marginBottom = Math.max(0, getStyleAsFloat(element, 'margin-bottom'));
// Keep drag placeholder's dimensions synced with the item's.
if (dragPlaceholder) dragPlaceholder.updateDimensions();
};
/**
* Fetch and store item's sort data.
*
* @private
*/
Item.prototype._refreshSortData = function () {
if (this._isDestroyed) return;
var data = (this._sortData = {});
var getters = this.getGrid()._settings.sortData;
var prop;
for (prop in getters) {
data[prop] = getters[prop](this, this._element);
}
};
/**
* Add item to layout.
*
* @private
*/
Item.prototype._addToLayout = function (left, top) {
if (this._isActive === true) return;
this._isActive = true;
this._left = left || 0;
this._top = top || 0;
};
/**
* Remove item from layout.
*
* @private
*/
Item.prototype._removeFromLayout = function () {
if (this._isActive === false) return;
this._isActive = false;
this._left = 0;
this._top = 0;
};
/**
* Check if the layout procedure can be skipped for the item.
*
* @private
* @param {Number} left
* @param {Number} top
* @returns {Boolean}
*/
Item.prototype._canSkipLayout = function (left, top) {
return (
this._left === left &&
this._top === top &&
!this._migrate._isActive &&
!this._layout._skipNextAnimation &&
!this._dragRelease.isJustReleased()
);
};
/**
* Set the provided left and top arguments as the item element's translate
* values in the DOM. This method keeps track of the currently applied
* translate values and skips the update operation if the provided values are
* identical to the currently applied values. Returns `false` if there was no
* need for update and `true` if the translate value was updated.
*
* @private
* @param {Number} left
* @param {Number} top
* @returns {Boolean}
*/
Item.prototype._setTranslate = function (left, top) {
if (this._tX === left && this._tY === top) return false;
this._tX = left;
this._tY = top;
this._element.style[transformProp] = getTranslateString(left, top);
return true;
};
/**
* Destroy item instance.
*
* @private
* @param {Boolean} [removeElement=false]
*/
Item.prototype._destroy = function (removeElement) {
if (this._isDestroyed) return;
var element = this._element;
var grid = this.getGrid();
var settings = grid._settings;
// Destroy handlers.
this._dragPlaceholder.destroy();
this._dragRelease.destroy();
this._migrate.destroy();
this._layout.destroy();
this._visibility.destroy();
if (this._drag) this._drag.destroy();
// Destroy emitter.
this._emitter.destroy();
// Remove item class.
removeClass(element, settings.itemClass);
// Remove element from DOM.
if (removeElement) element.parentNode.removeChild(element);
// Remove item/element pair from map.
if (ITEM_ELEMENT_MAP) ITEM_ELEMENT_MAP.delete(element);
// Reset state.
this._isActive = false;
this._isDestroyed = true;
};
export default Item;

1741
app/src/vendor/muuri-src/Item/ItemDrag.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,442 @@
/**
* Copyright (c) 2018-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import {
addPlaceholderLayoutTick,
cancelPlaceholderLayoutTick,
addPlaceholderResizeTick,
cancelPlaceholderResizeTick,
} from '../ticker';
import {
EVENT_BEFORE_SEND,
EVENT_DRAG_RELEASE_END,
EVENT_LAYOUT_START,
EVENT_HIDE_START,
} from '../constants';
import Animator from '../Animator/Animator';
import addClass from '../utils/addClass';
import getTranslateString from '../utils/getTranslateString';
import getTranslate from '../utils/getTranslate';
import isFunction from '../utils/isFunction';
import setStyles from '../utils/setStyles';
import removeClass from '../utils/removeClass';
import transformProp from '../utils/transformProp';
/**
* Drag placeholder.
*
* @class
* @param {Item} item
*/
function ItemDragPlaceholder(item) {
this._item = item;
this._animation = new Animator();
this._element = null;
this._className = '';
this._didMigrate = false;
this._resetAfterLayout = false;
this._left = 0;
this._top = 0;
this._transX = 0;
this._transY = 0;
this._nextTransX = 0;
this._nextTransY = 0;
// Bind animation handlers.
this._setupAnimation = this._setupAnimation.bind(this);
this._startAnimation = this._startAnimation.bind(this);
this._updateDimensions = this._updateDimensions.bind(this);
// Bind event handlers.
this._onLayoutStart = this._onLayoutStart.bind(this);
this._onLayoutEnd = this._onLayoutEnd.bind(this);
this._onReleaseEnd = this._onReleaseEnd.bind(this);
this._onMigrate = this._onMigrate.bind(this);
this._onHide = this._onHide.bind(this);
}
/**
* Private prototype methods
* *************************
*/
/**
* Update placeholder's dimensions to match the item's dimensions.
*
* @private
*/
ItemDragPlaceholder.prototype._updateDimensions = function () {
if (!this.isActive()) return;
setStyles(this._element, {
width: this._item._width + 'px',
height: this._item._height + 'px',
});
};
/**
* Move placeholder to a new position.
*
* @private
* @param {Item[]} items
* @param {Boolean} isInstant
*/
ItemDragPlaceholder.prototype._onLayoutStart = function (items, isInstant) {
var item = this._item;
// If the item is not part of the layout anymore reset placeholder.
if (items.indexOf(item) === -1) {
this.reset();
return;
}
var nextLeft = item._left;
var nextTop = item._top;
var currentLeft = this._left;
var currentTop = this._top;
// Keep track of item layout position.
this._left = nextLeft;
this._top = nextTop;
// If item's position did not change, and the item did not migrate and the
// layout is not instant and we can safely skip layout.
if (!isInstant && !this._didMigrate && currentLeft === nextLeft && currentTop === nextTop) {
return;
}
// Slots data is calculated with item margins added to them so we need to add
// item's left and top margin to the slot data to get the placeholder's
// next position.
var nextX = nextLeft + item._marginLeft;
var nextY = nextTop + item._marginTop;
// Just snap to new position without any animations if no animation is
// required or if placeholder moves between grids.
var grid = item.getGrid();
var animEnabled = !isInstant && grid._settings.layoutDuration > 0;
if (!animEnabled || this._didMigrate) {
// Cancel potential (queued) layout tick.
cancelPlaceholderLayoutTick(item._id);
// Snap placeholder to correct position.
this._element.style[transformProp] = getTranslateString(nextX, nextY);
this._animation.stop();
// Move placeholder inside correct container after migration.
if (this._didMigrate) {
grid.getElement().appendChild(this._element);
this._didMigrate = false;
}
return;
}
// Start the placeholder's layout animation in the next tick. We do this to
// avoid layout thrashing.
this._nextTransX = nextX;
this._nextTransY = nextY;
addPlaceholderLayoutTick(item._id, this._setupAnimation, this._startAnimation);
};
/**
* Prepare placeholder for layout animation.
*
* @private
*/
ItemDragPlaceholder.prototype._setupAnimation = function () {
if (!this.isActive()) return;
var translate = getTranslate(this._element);
this._transX = translate.x;
this._transY = translate.y;
};
/**
* Start layout animation.
*
* @private
*/
ItemDragPlaceholder.prototype._startAnimation = function () {
if (!this.isActive()) return;
var animation = this._animation;
var currentX = this._transX;
var currentY = this._transY;
var nextX = this._nextTransX;
var nextY = this._nextTransY;
// If placeholder is already in correct position let's just stop animation
// and be done with it.
if (currentX === nextX && currentY === nextY) {
if (animation.isAnimating()) {
this._element.style[transformProp] = getTranslateString(nextX, nextY);
animation.stop();
}
return;
}
// Otherwise let's start the animation.
var settings = this._item.getGrid()._settings;
var currentStyles = {};
var targetStyles = {};
currentStyles[transformProp] = getTranslateString(currentX, currentY);
targetStyles[transformProp] = getTranslateString(nextX, nextY);
animation.start(currentStyles, targetStyles, {
duration: settings.layoutDuration,
easing: settings.layoutEasing,
onFinish: this._onLayoutEnd,
});
};
/**
* Layout end handler.
*
* @private
*/
ItemDragPlaceholder.prototype._onLayoutEnd = function () {
if (this._resetAfterLayout) {
this.reset();
}
};
/**
* Drag end handler. This handler is called when dragReleaseEnd event is
* emitted and receives the event data as it's argument.
*
* @private
* @param {Item} item
*/
ItemDragPlaceholder.prototype._onReleaseEnd = function (item) {
if (item._id === this._item._id) {
// If the placeholder is not animating anymore we can safely reset it.
if (!this._animation.isAnimating()) {
this.reset();
return;
}
// If the placeholder item is still animating here, let's wait for it to
// finish it's animation.
this._resetAfterLayout = true;
}
};
/**
* Migration start handler. This handler is called when beforeSend event is
* emitted and receives the event data as it's argument.
*
* @private
* @param {Object} data
* @param {Item} data.item
* @param {Grid} data.fromGrid
* @param {Number} data.fromIndex
* @param {Grid} data.toGrid
* @param {Number} data.toIndex
*/
ItemDragPlaceholder.prototype._onMigrate = function (data) {
// Make sure we have a matching item.
if (data.item !== this._item) return;
var grid = this._item.getGrid();
var nextGrid = data.toGrid;
// Unbind listeners from current grid.
grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd);
grid.off(EVENT_LAYOUT_START, this._onLayoutStart);
grid.off(EVENT_BEFORE_SEND, this._onMigrate);
grid.off(EVENT_HIDE_START, this._onHide);
// Bind listeners to the next grid.
nextGrid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd);
nextGrid.on(EVENT_LAYOUT_START, this._onLayoutStart);
nextGrid.on(EVENT_BEFORE_SEND, this._onMigrate);
nextGrid.on(EVENT_HIDE_START, this._onHide);
// Mark the item as migrated.
this._didMigrate = true;
};
/**
* Reset placeholder if the associated item is hidden.
*
* @private
* @param {Item[]} items
*/
ItemDragPlaceholder.prototype._onHide = function (items) {
if (items.indexOf(this._item) > -1) this.reset();
};
/**
* Public prototype methods
* ************************
*/
/**
* Create placeholder. Note that this method only writes to DOM and does not
* read anything from DOM so it should not cause any additional layout
* thrashing when it's called at the end of the drag start procedure.
*
* @public
*/
ItemDragPlaceholder.prototype.create = function () {
// If we already have placeholder set up we can skip the initiation logic.
if (this.isActive()) {
this._resetAfterLayout = false;
return;
}
var item = this._item;
var grid = item.getGrid();
var settings = grid._settings;
var animation = this._animation;
// Keep track of layout position.
this._left = item._left;
this._top = item._top;
// Create placeholder element.
var element;
if (isFunction(settings.dragPlaceholder.createElement)) {
element = settings.dragPlaceholder.createElement(item);
} else {
element = document.createElement('div');
}
this._element = element;
// Update element to animation instance.
animation._element = element;
// Add placeholder class to the placeholder element.
this._className = settings.itemPlaceholderClass || '';
if (this._className) {
addClass(element, this._className);
}
// Set initial styles.
setStyles(element, {
position: 'absolute',
left: '0px',
top: '0px',
width: item._width + 'px',
height: item._height + 'px',
});
// Set initial position.
element.style[transformProp] = getTranslateString(
item._left + item._marginLeft,
item._top + item._marginTop
);
// Bind event listeners.
grid.on(EVENT_LAYOUT_START, this._onLayoutStart);
grid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd);
grid.on(EVENT_BEFORE_SEND, this._onMigrate);
grid.on(EVENT_HIDE_START, this._onHide);
// onCreate hook.
if (isFunction(settings.dragPlaceholder.onCreate)) {
settings.dragPlaceholder.onCreate(item, element);
}
// Insert the placeholder element to the grid.
grid.getElement().appendChild(element);
};
/**
* Reset placeholder data.
*
* @public
*/
ItemDragPlaceholder.prototype.reset = function () {
if (!this.isActive()) return;
var element = this._element;
var item = this._item;
var grid = item.getGrid();
var settings = grid._settings;
var animation = this._animation;
// Reset flag.
this._resetAfterLayout = false;
// Cancel potential (queued) layout tick.
cancelPlaceholderLayoutTick(item._id);
cancelPlaceholderResizeTick(item._id);
// Reset animation instance.
animation.stop();
animation._element = null;
// Unbind event listeners.
grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd);
grid.off(EVENT_LAYOUT_START, this._onLayoutStart);
grid.off(EVENT_BEFORE_SEND, this._onMigrate);
grid.off(EVENT_HIDE_START, this._onHide);
// Remove placeholder class from the placeholder element.
if (this._className) {
removeClass(element, this._className);
this._className = '';
}
// Remove element.
element.parentNode.removeChild(element);
this._element = null;
// onRemove hook. Note that here we use the current grid's onRemove callback
// so if the item has migrated during drag the onRemove method will not be
// the originating grid's method.
if (isFunction(settings.dragPlaceholder.onRemove)) {
settings.dragPlaceholder.onRemove(item, element);
}
};
/**
* Check if placeholder is currently active (visible).
*
* @public
* @returns {Boolean}
*/
ItemDragPlaceholder.prototype.isActive = function () {
return !!this._element;
};
/**
* Get placeholder element.
*
* @public
* @returns {?HTMLElement}
*/
ItemDragPlaceholder.prototype.getElement = function () {
return this._element;
};
/**
* Update placeholder's dimensions to match the item's dimensions. Note that
* the updating is done asynchronously in the next tick to avoid layout
* thrashing.
*
* @public
*/
ItemDragPlaceholder.prototype.updateDimensions = function () {
if (!this.isActive()) return;
addPlaceholderResizeTick(this._item._id, this._updateDimensions);
};
/**
* Destroy placeholder instance.
*
* @public
*/
ItemDragPlaceholder.prototype.destroy = function () {
this.reset();
this._animation.destroy();
this._item = this._animation = null;
};
export default ItemDragPlaceholder;

View File

@ -0,0 +1,183 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import { EVENT_DRAG_RELEASE_START, EVENT_DRAG_RELEASE_END } from '../constants';
import addClass from '../utils/addClass';
import getTranslate from '../utils/getTranslate';
import removeClass from '../utils/removeClass';
/**
* The release process handler constructor. Although this might seem as proper
* fit for the drag process this needs to be separated into it's own logic
* because there might be a scenario where drag is disabled, but the release
* process still needs to be implemented (dragging from a grid to another).
*
* @class
* @param {Item} item
*/
function ItemDragRelease(item) {
this._item = item;
this._isActive = false;
this._isDestroyed = false;
this._isPositioningStarted = false;
this._containerDiffX = 0;
this._containerDiffY = 0;
}
/**
* Public prototype methods
* ************************
*/
/**
* Start the release process of an item.
*
* @public
*/
ItemDragRelease.prototype.start = function () {
if (this._isDestroyed || this._isActive) return;
var item = this._item;
var grid = item.getGrid();
var settings = grid._settings;
this._isActive = true;
addClass(item._element, settings.itemReleasingClass);
if (!settings.dragRelease.useDragContainer) {
this._placeToGrid();
}
grid._emit(EVENT_DRAG_RELEASE_START, item);
// Let's start layout manually _only_ if there is no unfinished layout in
// about to finish.
if (!grid._nextLayoutData) item._layout.start(false);
};
/**
* End the release process of an item. This method can be used to abort an
* ongoing release process (animation) or finish the release process.
*
* @public
* @param {Boolean} [abort=false]
* - Should the release be aborted? When true, the release end event won't be
* emitted. Set to true only when you need to abort the release process
* while the item is animating to it's position.
* @param {Number} [left]
* - The element's current translateX value (optional).
* @param {Number} [top]
* - The element's current translateY value (optional).
*/
ItemDragRelease.prototype.stop = function (abort, left, top) {
if (this._isDestroyed || !this._isActive) return;
var item = this._item;
var grid = item.getGrid();
if (!abort && (left === undefined || top === undefined)) {
left = item._left;
top = item._top;
}
var didReparent = this._placeToGrid(left, top);
this._reset(didReparent);
if (!abort) grid._emit(EVENT_DRAG_RELEASE_END, item);
};
ItemDragRelease.prototype.isJustReleased = function () {
return this._isActive && this._isPositioningStarted === false;
};
/**
* Destroy instance.
*
* @public
*/
ItemDragRelease.prototype.destroy = function () {
if (this._isDestroyed) return;
this.stop(true);
this._item = null;
this._isDestroyed = true;
};
/**
* Private prototype methods
* *************************
*/
/**
* Move the element back to the grid container element if it does not exist
* there already.
*
* @private
* @param {Number} [left]
* - The element's current translateX value (optional).
* @param {Number} [top]
* - The element's current translateY value (optional).
* @returns {Boolean}
* - Returns `true` if the element was reparented.
*/
ItemDragRelease.prototype._placeToGrid = function (left, top) {
if (this._isDestroyed) return;
var item = this._item;
var element = item._element;
var container = item.getGrid()._element;
var didReparent = false;
if (element && element.dataset && element.dataset.mergeHidden === '1') {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
return false;
}
if (element.parentNode !== container) {
if (left === undefined || top === undefined) {
var translate = getTranslate(element);
left = translate.x - this._containerDiffX;
top = translate.y - this._containerDiffY;
}
container.appendChild(element);
item._setTranslate(left, top);
didReparent = true;
}
this._containerDiffX = 0;
this._containerDiffY = 0;
return didReparent;
};
/**
* Reset data and remove releasing class.
*
* @private
* @param {Boolean} [needsReflow]
*/
ItemDragRelease.prototype._reset = function (needsReflow) {
if (this._isDestroyed) return;
var item = this._item;
var releasingClass = item.getGrid()._settings.itemReleasingClass;
this._isActive = false;
this._isPositioningStarted = false;
this._containerDiffX = 0;
this._containerDiffY = 0;
// If the element was just reparented we need to do a forced reflow to remove
// the class gracefully.
if (releasingClass) {
// eslint-disable-next-line
if (needsReflow) item._element.clientWidth;
removeClass(item._element, releasingClass);
}
};
export default ItemDragRelease;

View File

@ -0,0 +1,315 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import { addLayoutTick, cancelLayoutTick } from '../ticker';
import Animator from '../Animator/Animator';
import addClass from '../utils/addClass';
import getTranslate from '../utils/getTranslate';
import getTranslateString from '../utils/getTranslateString';
import isFunction from '../utils/isFunction';
import removeClass from '../utils/removeClass';
import transformProp from '../utils/transformProp';
var MIN_ANIMATION_DISTANCE = 2;
/**
* Layout manager for Item instance, handles the positioning of an item.
*
* @class
* @param {Item} item
*/
function ItemLayout(item) {
var element = item._element;
var elementStyle = element.style;
this._item = item;
this._isActive = false;
this._isDestroyed = false;
this._isInterrupted = false;
this._currentStyles = {};
this._targetStyles = {};
this._nextLeft = 0;
this._nextTop = 0;
this._offsetLeft = 0;
this._offsetTop = 0;
this._skipNextAnimation = false;
this._animOptions = {
onFinish: this._finish.bind(this),
duration: 0,
easing: 0,
};
// Set element's initial position styles.
elementStyle.left = '0px';
elementStyle.top = '0px';
item._setTranslate(0, 0);
this._animation = new Animator(element);
this._queue = 'layout-' + item._id;
// Bind animation handlers and finish method.
this._setupAnimation = this._setupAnimation.bind(this);
this._startAnimation = this._startAnimation.bind(this);
}
/**
* Public prototype methods
* ************************
*/
/**
* Start item layout based on it's current data.
*
* @public
* @param {Boolean} instant
* @param {Function} [onFinish]
*/
ItemLayout.prototype.start = function (instant, onFinish) {
if (this._isDestroyed) return;
var item = this._item;
var release = item._dragRelease;
var gridSettings = item.getGrid()._settings;
var isPositioning = this._isActive;
var isJustReleased = release.isJustReleased();
var animDuration = isJustReleased
? gridSettings.dragRelease.duration
: gridSettings.layoutDuration;
var animEasing = isJustReleased ? gridSettings.dragRelease.easing : gridSettings.layoutEasing;
var animEnabled = !instant && !this._skipNextAnimation && animDuration > 0;
// If the item is currently positioning cancel potential queued layout tick
// and process current layout callback queue with interrupted flag on.
if (isPositioning) {
cancelLayoutTick(item._id);
item._emitter.burst(this._queue, true, item);
}
// Mark release positioning as started.
if (isJustReleased) release._isPositioningStarted = true;
// Push the callback to the callback queue.
if (isFunction(onFinish)) {
item._emitter.on(this._queue, onFinish);
}
// Reset animation skipping flag.
this._skipNextAnimation = false;
// If no animations are needed, easy peasy!
if (!animEnabled) {
this._updateOffsets();
item._setTranslate(this._nextLeft, this._nextTop);
this._animation.stop();
this._finish();
return;
}
// Let's make sure an ongoing animation's callback is cancelled before going
// further. Without this there's a chance that the animation will finish
// before the next tick and mess up our logic.
if (this._animation.isAnimating()) {
this._animation._animation.onfinish = null;
}
// Kick off animation to be started in the next tick.
this._isActive = true;
this._animOptions.easing = animEasing;
this._animOptions.duration = animDuration;
this._isInterrupted = isPositioning;
addLayoutTick(item._id, this._setupAnimation, this._startAnimation);
};
/**
* Stop item's position animation if it is currently animating.
*
* @public
* @param {Boolean} processCallbackQueue
* @param {Number} [left]
* @param {Number} [top]
*/
ItemLayout.prototype.stop = function (processCallbackQueue, left, top) {
if (this._isDestroyed || !this._isActive) return;
var item = this._item;
// Cancel animation init.
cancelLayoutTick(item._id);
// Stop animation.
if (this._animation.isAnimating()) {
if (left === undefined || top === undefined) {
var translate = getTranslate(item._element);
left = translate.x;
top = translate.y;
}
item._setTranslate(left, top);
this._animation.stop();
}
// Remove positioning class.
removeClass(item._element, item.getGrid()._settings.itemPositioningClass);
// Reset active state.
this._isActive = false;
// Process callback queue if needed.
if (processCallbackQueue) {
item._emitter.burst(this._queue, true, item);
}
};
/**
* Destroy the instance and stop current animation if it is running.
*
* @public
*/
ItemLayout.prototype.destroy = function () {
if (this._isDestroyed) return;
var elementStyle = this._item._element.style;
this.stop(true, 0, 0);
this._item._emitter.clear(this._queue);
this._animation.destroy();
elementStyle[transformProp] = '';
elementStyle.left = '';
elementStyle.top = '';
this._item = null;
this._currentStyles = null;
this._targetStyles = null;
this._animOptions = null;
this._isDestroyed = true;
};
/**
* Private prototype methods
* *************************
*/
/**
* Calculate and update item's current layout offset data.
*
* @private
*/
ItemLayout.prototype._updateOffsets = function () {
if (this._isDestroyed) return;
var item = this._item;
var migrate = item._migrate;
var release = item._dragRelease;
this._offsetLeft = release._isActive
? release._containerDiffX
: migrate._isActive
? migrate._containerDiffX
: 0;
this._offsetTop = release._isActive
? release._containerDiffY
: migrate._isActive
? migrate._containerDiffY
: 0;
this._nextLeft = this._item._left + this._offsetLeft;
this._nextTop = this._item._top + this._offsetTop;
};
/**
* Finish item layout procedure.
*
* @private
*/
ItemLayout.prototype._finish = function () {
if (this._isDestroyed) return;
var item = this._item;
var migrate = item._migrate;
var release = item._dragRelease;
// Update internal translate values.
item._tX = this._nextLeft;
item._tY = this._nextTop;
// Mark the item as inactive and remove positioning classes.
if (this._isActive) {
this._isActive = false;
removeClass(item._element, item.getGrid()._settings.itemPositioningClass);
}
// Finish up release and migration.
if (release._isActive) release.stop();
if (migrate._isActive) migrate.stop();
// Process the callback queue.
item._emitter.burst(this._queue, false, item);
};
/**
* Prepare item for layout animation.
*
* @private
*/
ItemLayout.prototype._setupAnimation = function () {
var item = this._item;
if (item._tX === undefined || item._tY === undefined) {
var translate = getTranslate(item._element);
item._tX = translate.x;
item._tY = translate.y;
}
};
/**
* Start layout animation.
*
* @private
*/
ItemLayout.prototype._startAnimation = function () {
var item = this._item;
var settings = item.getGrid()._settings;
var isInstant = this._animOptions.duration <= 0;
// Let's update the offset data and target styles.
this._updateOffsets();
var xDiff = Math.abs(item._left - (item._tX - this._offsetLeft));
var yDiff = Math.abs(item._top - (item._tY - this._offsetTop));
// If there is no need for animation or if the item is already in correct
// position (or near it) let's finish the process early.
if (isInstant || (xDiff < MIN_ANIMATION_DISTANCE && yDiff < MIN_ANIMATION_DISTANCE)) {
if (xDiff || yDiff || this._isInterrupted) {
item._setTranslate(this._nextLeft, this._nextTop);
}
this._animation.stop();
this._finish();
return;
}
// Set item's positioning class if needed.
if (!this._isInterrupted) {
addClass(item._element, settings.itemPositioningClass);
}
// Get current/next styles for animation.
this._currentStyles[transformProp] = getTranslateString(item._tX, item._tY);
this._targetStyles[transformProp] = getTranslateString(this._nextLeft, this._nextTop);
// Set internal translation values to undefined for the duration of the
// animation since they will be changing on each animation frame for the
// duration of the animation and tracking them would mean reading the DOM on
// each frame, which is pretty darn expensive.
item._tX = item._tY = undefined;
// Start animation.
this._animation.start(this._currentStyles, this._targetStyles, this._animOptions);
};
export default ItemLayout;

View File

@ -0,0 +1,287 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import { EVENT_BEFORE_SEND, EVENT_BEFORE_RECEIVE, EVENT_SEND, EVENT_RECEIVE } from '../constants';
import ItemDrag from './ItemDrag';
import addClass from '../utils/addClass';
import getOffsetDiff from '../utils/getOffsetDiff';
import getTranslate from '../utils/getTranslate';
import arrayInsert from '../utils/arrayInsert';
import normalizeArrayIndex from '../utils/normalizeArrayIndex';
import removeClass from '../utils/removeClass';
/**
* The migrate process handler constructor.
*
* @class
* @param {Item} item
*/
function ItemMigrate(item) {
// Private props.
this._item = item;
this._isActive = false;
this._isDestroyed = false;
this._container = false;
this._containerDiffX = 0;
this._containerDiffY = 0;
}
/**
* Public prototype methods
* ************************
*/
/**
* Start the migrate process of an item.
*
* @public
* @param {Grid} targetGrid
* @param {(HTMLElement|Number|Item)} position
* @param {HTMLElement} [container]
*/
ItemMigrate.prototype.start = function (targetGrid, position, container) {
if (this._isDestroyed) return;
var item = this._item;
var element = item._element;
var isActive = item.isActive();
var isVisible = item.isVisible();
var grid = item.getGrid();
var settings = grid._settings;
var targetSettings = targetGrid._settings;
var targetElement = targetGrid._element;
var targetItems = targetGrid._items;
var currentIndex = grid._items.indexOf(item);
var targetContainer = container || document.body;
var targetIndex;
var targetItem;
var currentContainer;
var offsetDiff;
var containerDiff;
var translate;
var translateX;
var translateY;
var currentVisClass;
var nextVisClass;
// Get target index.
if (typeof position === 'number') {
targetIndex = normalizeArrayIndex(targetItems, position, 1);
} else {
targetItem = targetGrid.getItem(position);
if (!targetItem) return;
targetIndex = targetItems.indexOf(targetItem);
}
// Get current translateX and translateY values if needed.
if (item.isPositioning() || this._isActive || item.isReleasing()) {
translate = getTranslate(element);
translateX = translate.x;
translateY = translate.y;
}
// Abort current positioning.
if (item.isPositioning()) {
item._layout.stop(true, translateX, translateY);
}
// Abort current migration.
if (this._isActive) {
translateX -= this._containerDiffX;
translateY -= this._containerDiffY;
this.stop(true, translateX, translateY);
}
// Abort current release.
if (item.isReleasing()) {
translateX -= item._dragRelease._containerDiffX;
translateY -= item._dragRelease._containerDiffY;
item._dragRelease.stop(true, translateX, translateY);
}
// Stop current visibility animation.
item._visibility.stop(true);
// Destroy current drag.
if (item._drag) item._drag.destroy();
// Emit beforeSend event.
if (grid._hasListeners(EVENT_BEFORE_SEND)) {
grid._emit(EVENT_BEFORE_SEND, {
item: item,
fromGrid: grid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex,
});
}
// Emit beforeReceive event.
if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) {
targetGrid._emit(EVENT_BEFORE_RECEIVE, {
item: item,
fromGrid: grid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex,
});
}
// Update item class.
if (settings.itemClass !== targetSettings.itemClass) {
removeClass(element, settings.itemClass);
addClass(element, targetSettings.itemClass);
}
// Update visibility class.
currentVisClass = isVisible ? settings.itemVisibleClass : settings.itemHiddenClass;
nextVisClass = isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass;
if (currentVisClass !== nextVisClass) {
removeClass(element, currentVisClass);
addClass(element, nextVisClass);
}
// Move item instance from current grid to target grid.
grid._items.splice(currentIndex, 1);
arrayInsert(targetItems, item, targetIndex);
// Update item's grid id reference.
item._gridId = targetGrid._id;
// If item is active we need to move the item inside the target container for
// the duration of the (potential) animation if it's different than the
// current container.
if (isActive) {
currentContainer = element.parentNode;
if (targetContainer !== currentContainer) {
targetContainer.appendChild(element);
offsetDiff = getOffsetDiff(targetContainer, currentContainer, true);
if (!translate) {
translate = getTranslate(element);
translateX = translate.x;
translateY = translate.y;
}
item._setTranslate(translateX + offsetDiff.left, translateY + offsetDiff.top);
}
}
// If item is not active let's just append it to the target grid's element.
else {
targetElement.appendChild(element);
}
// Update child element's styles to reflect the current visibility state.
item._visibility.setStyles(
isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles
);
// Get offset diff for the migration data, if the item is active.
if (isActive) {
containerDiff = getOffsetDiff(targetContainer, targetElement, true);
}
// Update item's cached dimensions.
item._refreshDimensions();
// Reset item's sort data.
item._sortData = null;
// Create new drag handler.
item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null;
// Setup migration data.
if (isActive) {
this._isActive = true;
this._container = targetContainer;
this._containerDiffX = containerDiff.left;
this._containerDiffY = containerDiff.top;
} else {
this._isActive = false;
this._container = null;
this._containerDiffX = 0;
this._containerDiffY = 0;
}
// Emit send event.
if (grid._hasListeners(EVENT_SEND)) {
grid._emit(EVENT_SEND, {
item: item,
fromGrid: grid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex,
});
}
// Emit receive event.
if (targetGrid._hasListeners(EVENT_RECEIVE)) {
targetGrid._emit(EVENT_RECEIVE, {
item: item,
fromGrid: grid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex,
});
}
};
/**
* End the migrate process of an item. This method can be used to abort an
* ongoing migrate process (animation) or finish the migrate process.
*
* @public
* @param {Boolean} [abort=false]
* - Should the migration be aborted?
* @param {Number} [left]
* - The element's current translateX value (optional).
* @param {Number} [top]
* - The element's current translateY value (optional).
*/
ItemMigrate.prototype.stop = function (abort, left, top) {
if (this._isDestroyed || !this._isActive) return;
var item = this._item;
var element = item._element;
var grid = item.getGrid();
var gridElement = grid._element;
var translate;
if (this._container !== gridElement) {
if (left === undefined || top === undefined) {
if (abort) {
translate = getTranslate(element);
left = translate.x - this._containerDiffX;
top = translate.y - this._containerDiffY;
} else {
left = item._left;
top = item._top;
}
}
gridElement.appendChild(element);
item._setTranslate(left, top);
}
this._isActive = false;
this._container = null;
this._containerDiffX = 0;
this._containerDiffY = 0;
};
/**
* Destroy instance.
*
* @public
*/
ItemMigrate.prototype.destroy = function () {
if (this._isDestroyed) return;
this.stop(true);
this._item = null;
this._isDestroyed = true;
};
export default ItemMigrate;

View File

@ -0,0 +1,325 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import { addVisibilityTick, cancelVisibilityTick } from '../ticker';
import Animator from '../Animator/Animator';
import addClass from '../utils/addClass';
import getCurrentStyles from '../utils/getCurrentStyles';
import isFunction from '../utils/isFunction';
import removeClass from '../utils/removeClass';
import setStyles from '../utils/setStyles';
/**
* Visibility manager for Item instance, handles visibility of an item.
*
* @class
* @param {Item} item
*/
function ItemVisibility(item) {
var isActive = item._isActive;
var element = item._element;
var childElement = element.children[0];
var settings = item.getGrid()._settings;
if (!childElement) {
throw new Error('No valid child element found within item element.');
}
this._item = item;
this._isDestroyed = false;
this._isHidden = !isActive;
this._isHiding = false;
this._isShowing = false;
this._childElement = childElement;
this._currentStyleProps = [];
this._animation = new Animator(childElement);
this._queue = 'visibility-' + item._id;
this._finishShow = this._finishShow.bind(this);
this._finishHide = this._finishHide.bind(this);
element.style.display = isActive ? '' : 'none';
addClass(element, isActive ? settings.itemVisibleClass : settings.itemHiddenClass);
this.setStyles(isActive ? settings.visibleStyles : settings.hiddenStyles);
}
/**
* Public prototype methods
* ************************
*/
/**
* Show item.
*
* @public
* @param {Boolean} instant
* @param {Function} [onFinish]
*/
ItemVisibility.prototype.show = function (instant, onFinish) {
if (this._isDestroyed) return;
var item = this._item;
var element = item._element;
var callback = isFunction(onFinish) ? onFinish : null;
var grid = item.getGrid();
var settings = grid._settings;
// If item is visible call the callback and be done with it.
if (!this._isShowing && !this._isHidden) {
callback && callback(false, item);
return;
}
// If item is showing and does not need to be shown instantly, let's just
// push callback to the callback queue and be done with it.
if (this._isShowing && !instant) {
callback && item._emitter.on(this._queue, callback);
return;
}
// If the item is hiding or hidden process the current visibility callback
// queue with the interrupted flag active, update classes and set display
// to block if necessary.
if (!this._isShowing) {
item._emitter.burst(this._queue, true, item);
removeClass(element, settings.itemHiddenClass);
addClass(element, settings.itemVisibleClass);
if (!this._isHiding) element.style.display = '';
}
// Push callback to the callback queue.
callback && item._emitter.on(this._queue, callback);
// Update visibility states.
this._isShowing = true;
this._isHiding = this._isHidden = false;
// Finally let's start show animation.
this._startAnimation(true, instant, this._finishShow);
};
/**
* Hide item.
*
* @public
* @param {Boolean} instant
* @param {Function} [onFinish]
*/
ItemVisibility.prototype.hide = function (instant, onFinish) {
if (this._isDestroyed) return;
var item = this._item;
var element = item._element;
var callback = isFunction(onFinish) ? onFinish : null;
var grid = item.getGrid();
var settings = grid._settings;
// If item is already hidden call the callback and be done with it.
if (!this._isHiding && this._isHidden) {
callback && callback(false, item);
return;
}
// If item is hiding and does not need to be hidden instantly, let's just
// push callback to the callback queue and be done with it.
if (this._isHiding && !instant) {
callback && item._emitter.on(this._queue, callback);
return;
}
// If the item is showing or visible process the current visibility callback
// queue with the interrupted flag active, update classes and set display
// to block if necessary.
if (!this._isHiding) {
item._emitter.burst(this._queue, true, item);
addClass(element, settings.itemHiddenClass);
removeClass(element, settings.itemVisibleClass);
}
// Push callback to the callback queue.
callback && item._emitter.on(this._queue, callback);
// Update visibility states.
this._isHidden = this._isHiding = true;
this._isShowing = false;
// Finally let's start hide animation.
this._startAnimation(false, instant, this._finishHide);
};
/**
* Stop current hiding/showing process.
*
* @public
* @param {Boolean} processCallbackQueue
*/
ItemVisibility.prototype.stop = function (processCallbackQueue) {
if (this._isDestroyed) return;
if (!this._isHiding && !this._isShowing) return;
var item = this._item;
cancelVisibilityTick(item._id);
this._animation.stop();
if (processCallbackQueue) {
item._emitter.burst(this._queue, true, item);
}
};
/**
* Reset all existing visibility styles and apply new visibility styles to the
* visibility element. This method should be used to set styles when there is a
* chance that the current style properties differ from the new ones (basically
* on init and on migrations).
*
* @public
* @param {Object} styles
*/
ItemVisibility.prototype.setStyles = function (styles) {
var childElement = this._childElement;
var currentStyleProps = this._currentStyleProps;
this._removeCurrentStyles();
for (var prop in styles) {
currentStyleProps.push(prop);
childElement.style[prop] = styles[prop];
}
};
/**
* Destroy the instance and stop current animation if it is running.
*
* @public
*/
ItemVisibility.prototype.destroy = function () {
if (this._isDestroyed) return;
var item = this._item;
var element = item._element;
var grid = item.getGrid();
var settings = grid._settings;
this.stop(true);
item._emitter.clear(this._queue);
this._animation.destroy();
this._removeCurrentStyles();
removeClass(element, settings.itemVisibleClass);
removeClass(element, settings.itemHiddenClass);
element.style.display = '';
// Reset state.
this._isHiding = this._isShowing = false;
this._isDestroyed = this._isHidden = true;
};
/**
* Private prototype methods
* *************************
*/
/**
* Start visibility animation.
*
* @private
* @param {Boolean} toVisible
* @param {Boolean} [instant]
* @param {Function} [onFinish]
*/
ItemVisibility.prototype._startAnimation = function (toVisible, instant, onFinish) {
if (this._isDestroyed) return;
var item = this._item;
var animation = this._animation;
var childElement = this._childElement;
var settings = item.getGrid()._settings;
var targetStyles = toVisible ? settings.visibleStyles : settings.hiddenStyles;
var duration = toVisible ? settings.showDuration : settings.hideDuration;
var easing = toVisible ? settings.showEasing : settings.hideEasing;
var isInstant = instant || duration <= 0;
var currentStyles;
// No target styles? Let's quit early.
if (!targetStyles) {
onFinish && onFinish();
return;
}
// Cancel queued visibility tick.
cancelVisibilityTick(item._id);
// If we need to apply the styles instantly without animation.
if (isInstant) {
setStyles(childElement, targetStyles);
animation.stop();
onFinish && onFinish();
return;
}
// Let's make sure an ongoing animation's callback is cancelled before going
// further. Without this there's a chance that the animation will finish
// before the next tick and mess up our logic.
if (animation.isAnimating()) {
animation._animation.onfinish = null;
}
// Start the animation in the next tick (to avoid layout thrashing).
addVisibilityTick(
item._id,
function () {
currentStyles = getCurrentStyles(childElement, targetStyles);
},
function () {
animation.start(currentStyles, targetStyles, {
duration: duration,
easing: easing,
onFinish: onFinish,
});
}
);
};
/**
* Finish show procedure.
*
* @private
*/
ItemVisibility.prototype._finishShow = function () {
if (this._isHidden) return;
this._isShowing = false;
this._item._emitter.burst(this._queue, false, this._item);
};
/**
* Finish hide procedure.
*
* @private
*/
ItemVisibility.prototype._finishHide = function () {
if (!this._isHidden) return;
var item = this._item;
this._isHiding = false;
item._layout.stop(true, 0, 0);
item._element.style.display = 'none';
item._emitter.burst(this._queue, false, item);
};
/**
* Remove currently applied visibility related inline style properties.
*
* @private
*/
ItemVisibility.prototype._removeCurrentStyles = function () {
var childElement = this._childElement;
var currentStyleProps = this._currentStyleProps;
for (var i = 0; i < currentStyleProps.length; i++) {
childElement.style[currentStyleProps[i]] = '';
}
currentStyleProps.length = 0;
};
export default ItemVisibility;

View File

@ -0,0 +1,19 @@
Copyright (c) 2016, Niklas Rämö
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,283 @@
/**
* Muuri Packer
* Copyright (c) 2016-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/Packer/LICENSE.md
*/
import PackerProcessor, {
createWorkerProcessors,
destroyWorkerProcessors,
isWorkerProcessorsSupported,
} from './PackerProcessor';
export var FILL_GAPS = 1;
export var HORIZONTAL = 2;
export var ALIGN_RIGHT = 4;
export var ALIGN_BOTTOM = 8;
export var ROUNDING = 16;
export var PACKET_INDEX_ID = 0;
export var PACKET_INDEX_WIDTH = 1;
export var PACKET_INDEX_HEIGHT = 2;
export var PACKET_INDEX_OPTIONS = 3;
export var PACKET_HEADER_SLOTS = 4;
/**
* @class
* @param {Number} [numWorkers=0]
* @param {Object} [options]
* @param {Boolean} [options.fillGaps=false]
* @param {Boolean} [options.horizontal=false]
* @param {Boolean} [options.alignRight=false]
* @param {Boolean} [options.alignBottom=false]
* @param {Boolean} [options.rounding=false]
*/
function Packer(numWorkers, options) {
this._options = 0;
this._processor = null;
this._layoutQueue = [];
this._layouts = {};
this._layoutCallbacks = {};
this._layoutWorkers = {};
this._layoutWorkerData = {};
this._workers = [];
this._onWorkerMessage = this._onWorkerMessage.bind(this);
// Set initial options.
this.setOptions(options);
// Init the worker(s) or the processor if workers can't be used.
numWorkers = typeof numWorkers === 'number' ? Math.max(0, numWorkers) : 0;
if (numWorkers && isWorkerProcessorsSupported()) {
try {
this._workers = createWorkerProcessors(numWorkers, this._onWorkerMessage);
} catch (e) {
this._processor = new PackerProcessor();
}
} else {
this._processor = new PackerProcessor();
}
}
Packer.prototype._sendToWorker = function () {
if (!this._layoutQueue.length || !this._workers.length) return;
var layoutId = this._layoutQueue.shift();
var worker = this._workers.pop();
var data = this._layoutWorkerData[layoutId];
delete this._layoutWorkerData[layoutId];
this._layoutWorkers[layoutId] = worker;
worker.postMessage(data.buffer, [data.buffer]);
};
Packer.prototype._onWorkerMessage = function (msg) {
var data = new Float32Array(msg.data);
var layoutId = data[PACKET_INDEX_ID];
var layout = this._layouts[layoutId];
var callback = this._layoutCallbacks[layoutId];
var worker = this._layoutWorkers[layoutId];
if (layout) delete this._layouts[layoutId];
if (callback) delete this._layoutCallbacks[layoutId];
if (worker) delete this._layoutWorkers[layoutId];
if (layout && callback) {
layout.width = data[PACKET_INDEX_WIDTH];
layout.height = data[PACKET_INDEX_HEIGHT];
layout.slots = data.subarray(PACKET_HEADER_SLOTS, data.length);
this._finalizeLayout(layout);
callback(layout);
}
if (worker) {
this._workers.push(worker);
this._sendToWorker();
}
};
Packer.prototype._finalizeLayout = function (layout) {
var grid = layout._grid;
var isHorizontal = layout._settings & HORIZONTAL;
var isBorderBox = grid._boxSizing === 'border-box';
delete layout._grid;
delete layout._settings;
layout.styles = {};
if (isHorizontal) {
layout.styles.width =
(isBorderBox ? layout.width + grid._borderLeft + grid._borderRight : layout.width) + 'px';
} else {
layout.styles.height =
(isBorderBox ? layout.height + grid._borderTop + grid._borderBottom : layout.height) + 'px';
}
return layout;
};
/**
* @public
* @param {Object} [options]
* @param {Boolean} [options.fillGaps]
* @param {Boolean} [options.horizontal]
* @param {Boolean} [options.alignRight]
* @param {Boolean} [options.alignBottom]
* @param {Boolean} [options.rounding]
*/
Packer.prototype.setOptions = function (options) {
if (!options) return;
var fillGaps;
if (typeof options.fillGaps === 'boolean') {
fillGaps = options.fillGaps ? FILL_GAPS : 0;
} else {
fillGaps = this._options & FILL_GAPS;
}
var horizontal;
if (typeof options.horizontal === 'boolean') {
horizontal = options.horizontal ? HORIZONTAL : 0;
} else {
horizontal = this._options & HORIZONTAL;
}
var alignRight;
if (typeof options.alignRight === 'boolean') {
alignRight = options.alignRight ? ALIGN_RIGHT : 0;
} else {
alignRight = this._options & ALIGN_RIGHT;
}
var alignBottom;
if (typeof options.alignBottom === 'boolean') {
alignBottom = options.alignBottom ? ALIGN_BOTTOM : 0;
} else {
alignBottom = this._options & ALIGN_BOTTOM;
}
var rounding;
if (typeof options.rounding === 'boolean') {
rounding = options.rounding ? ROUNDING : 0;
} else {
rounding = this._options & ROUNDING;
}
this._options = fillGaps | horizontal | alignRight | alignBottom | rounding;
};
/**
* @public
* @param {Grid} grid
* @param {Number} layoutId
* @param {Item[]} items
* @param {Number} width
* @param {Number} height
* @param {Function} callback
* @returns {?Function}
*/
Packer.prototype.createLayout = function (grid, layoutId, items, width, height, callback) {
if (this._layouts[layoutId]) {
throw new Error('A layout with the provided id is currently being processed.');
}
var horizontal = this._options & HORIZONTAL;
var layout = {
id: layoutId,
items: items,
slots: null,
width: horizontal ? 0 : width,
height: !horizontal ? 0 : height,
// Temporary data, which will be removed before sending the layout data
// outside of Packer's context.
_grid: grid,
_settings: this._options,
};
// If there are no items let's call the callback immediately.
if (!items.length) {
layout.slots = [];
this._finalizeLayout(layout);
callback(layout);
return;
}
// Create layout synchronously if needed.
if (this._processor) {
layout.slots = window.Float32Array
? new Float32Array(items.length * 2)
: new Array(items.length * 2);
this._processor.computeLayout(layout, layout._settings);
this._finalizeLayout(layout);
callback(layout);
return;
}
// Worker data.
var data = new Float32Array(PACKET_HEADER_SLOTS + items.length * 2);
// Worker data header.
data[PACKET_INDEX_ID] = layoutId;
data[PACKET_INDEX_WIDTH] = layout.width;
data[PACKET_INDEX_HEIGHT] = layout.height;
data[PACKET_INDEX_OPTIONS] = layout._settings;
// Worker data items.
var i, j, item;
for (i = 0, j = PACKET_HEADER_SLOTS - 1, item; i < items.length; i++) {
item = items[i];
data[++j] = item._width + item._marginLeft + item._marginRight;
data[++j] = item._height + item._marginTop + item._marginBottom;
}
this._layoutQueue.push(layoutId);
this._layouts[layoutId] = layout;
this._layoutCallbacks[layoutId] = callback;
this._layoutWorkerData[layoutId] = data;
this._sendToWorker();
return this.cancelLayout.bind(this, layoutId);
};
/**
* @public
* @param {Number} layoutId
*/
Packer.prototype.cancelLayout = function (layoutId) {
var layout = this._layouts[layoutId];
if (!layout) return;
delete this._layouts[layoutId];
delete this._layoutCallbacks[layoutId];
if (this._layoutWorkerData[layoutId]) {
delete this._layoutWorkerData[layoutId];
var queueIndex = this._layoutQueue.indexOf(layoutId);
if (queueIndex > -1) this._layoutQueue.splice(queueIndex, 1);
}
};
/**
* @public
*/
Packer.prototype.destroy = function () {
// Move all currently used workers back in the workers array.
for (var key in this._layoutWorkers) {
this._workers.push(this._layoutWorkers[key]);
}
// Destroy all instance's workers.
destroyWorkerProcessors(this._workers);
// Reset data.
this._workers.length = 0;
this._layoutQueue.length = 0;
this._layouts = {};
this._layoutCallbacks = {};
this._layoutWorkers = {};
this._layoutWorkerData = {};
};
export default Packer;

View File

@ -0,0 +1,597 @@
/**
* Muuri Packer
* Copyright (c) 2016-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/Packer/LICENSE.md
*/
function createPackerProcessor(isWorker) {
var FILL_GAPS = 1;
var HORIZONTAL = 2;
var ALIGN_RIGHT = 4;
var ALIGN_BOTTOM = 8;
var ROUNDING = 16;
var EPS = 0.001;
var MIN_SLOT_SIZE = 0.5;
// Rounds number first to three decimal precision and then floors the result
// to two decimal precision.
// Math.floor(Math.round(number * 1000) / 10) / 100
function roundNumber(number) {
return ((((number * 1000 + 0.5) << 0) / 10) << 0) / 100;
}
/**
* @class
*/
function PackerProcessor() {
this.currentRects = [];
this.nextRects = [];
this.rectTarget = {};
this.rectStore = [];
this.slotSizes = [];
this.rectId = 0;
this.slotIndex = -1;
this.slotData = { left: 0, top: 0, width: 0, height: 0 };
this.sortRectsLeftTop = this.sortRectsLeftTop.bind(this);
this.sortRectsTopLeft = this.sortRectsTopLeft.bind(this);
}
/**
* Takes a layout object as an argument and computes positions (slots) for the
* layout items. Also computes the final width and height of the layout. The
* provided layout object's slots array is mutated as well as the width and
* height properties.
*
* @param {Object} layout
* @param {Number} layout.width
* - The start (current) width of the layout in pixels.
* @param {Number} layout.height
* - The start (current) height of the layout in pixels.
* @param {(Item[]|Number[])} layout.items
* - List of Muuri.Item instances or a list of item dimensions
* (e.g [ item1Width, item1Height, item2Width, item2Height, ... ]).
* @param {(Array|Float32Array)} layout.slots
* - An Array/Float32Array instance which's length should equal to
* the amount of items times two. The position (width and height) of each
* item will be written into this array.
* @param {Number} settings
* - The layout's settings as bitmasks.
* @returns {Object}
*/
PackerProcessor.prototype.computeLayout = function (layout, settings) {
var items = layout.items;
var slots = layout.slots;
var fillGaps = !!(settings & FILL_GAPS);
var horizontal = !!(settings & HORIZONTAL);
var alignRight = !!(settings & ALIGN_RIGHT);
var alignBottom = !!(settings & ALIGN_BOTTOM);
var rounding = !!(settings & ROUNDING);
var isPreProcessed = typeof items[0] === 'number';
var i, bump, item, slotWidth, slotHeight, slot;
// No need to go further if items do not exist.
if (!items.length) return layout;
// Compute slots for the items.
bump = isPreProcessed ? 2 : 1;
for (i = 0; i < items.length; i += bump) {
// If items are pre-processed it means that items array contains only
// the raw dimensions of the items. Otherwise we assume it is an array
// of normal Muuri items.
if (isPreProcessed) {
slotWidth = items[i];
slotHeight = items[i + 1];
} else {
item = items[i];
slotWidth = item._width + item._marginLeft + item._marginRight;
slotHeight = item._height + item._marginTop + item._marginBottom;
}
// If rounding is enabled let's round the item's width and height to
// make the layout algorithm a bit more stable. This has a performance
// cost so don't use this if not necessary.
if (rounding) {
slotWidth = roundNumber(slotWidth);
slotHeight = roundNumber(slotHeight);
}
// Get slot data.
slot = this.computeNextSlot(layout, slotWidth, slotHeight, fillGaps, horizontal);
// Update layout width/height.
if (horizontal) {
if (slot.left + slot.width > layout.width) {
layout.width = slot.left + slot.width;
}
} else {
if (slot.top + slot.height > layout.height) {
layout.height = slot.top + slot.height;
}
}
// Add item slot data to layout slots.
slots[++this.slotIndex] = slot.left;
slots[++this.slotIndex] = slot.top;
// Store the size too (for later usage) if needed.
if (alignRight || alignBottom) {
this.slotSizes.push(slot.width, slot.height);
}
}
// If the alignment is set to right we need to adjust the results.
if (alignRight) {
for (i = 0; i < slots.length; i += 2) {
slots[i] = layout.width - (slots[i] + this.slotSizes[i]);
}
}
// If the alignment is set to bottom we need to adjust the results.
if (alignBottom) {
for (i = 1; i < slots.length; i += 2) {
slots[i] = layout.height - (slots[i] + this.slotSizes[i]);
}
}
// Reset stuff.
this.slotSizes.length = 0;
this.currentRects.length = 0;
this.nextRects.length = 0;
this.rectStore.length = 0;
this.rectId = 0;
this.slotIndex = -1;
return layout;
};
/**
* Calculate next slot in the layout. Returns a slot object with position and
* dimensions data. The returned object is reused between calls.
*
* @param {Object} layout
* @param {Number} slotWidth
* @param {Number} slotHeight
* @param {Boolean} fillGaps
* @param {Boolean} horizontal
* @returns {Object}
*/
PackerProcessor.prototype.computeNextSlot = function (
layout,
slotWidth,
slotHeight,
fillGaps,
horizontal
) {
var slot = this.slotData;
var currentRects = this.currentRects;
var nextRects = this.nextRects;
var ignoreCurrentRects = false;
var rect;
var rectId;
var shards;
var i;
var j;
// Reset new slots.
nextRects.length = 0;
// Set item slot initial data.
slot.left = null;
slot.top = null;
slot.width = slotWidth;
slot.height = slotHeight;
// Try to find position for the slot from the existing free spaces in the
// layout.
for (i = 0; i < currentRects.length; i++) {
rectId = currentRects[i];
if (!rectId) continue;
rect = this.getRect(rectId);
if (slot.width <= rect.width + EPS && slot.height <= rect.height + EPS) {
slot.left = rect.left;
slot.top = rect.top;
break;
}
}
// If no position was found for the slot let's position the slot to
// the bottom left (in vertical mode) or top right (in horizontal mode) of
// the layout.
if (slot.left === null) {
if (horizontal) {
slot.left = layout.width;
slot.top = 0;
} else {
slot.left = 0;
slot.top = layout.height;
}
// If gaps don't need filling let's throw away all the current free spaces
// (currentRects).
if (!fillGaps) {
ignoreCurrentRects = true;
}
}
// In vertical mode, if the slot's bottom overlaps the layout's bottom.
if (!horizontal && slot.top + slot.height > layout.height + EPS) {
// If slot is not aligned to the left edge, create a new free space to the
// left of the slot.
if (slot.left > MIN_SLOT_SIZE) {
nextRects.push(this.addRect(0, layout.height, slot.left, Infinity));
}
// If slot is not aligned to the right edge, create a new free space to
// the right of the slot.
if (slot.left + slot.width < layout.width - MIN_SLOT_SIZE) {
nextRects.push(
this.addRect(
slot.left + slot.width,
layout.height,
layout.width - slot.left - slot.width,
Infinity
)
);
}
// Update layout height.
layout.height = slot.top + slot.height;
}
// In horizontal mode, if the slot's right overlaps the layout's right edge.
if (horizontal && slot.left + slot.width > layout.width + EPS) {
// If slot is not aligned to the top, create a new free space above the
// slot.
if (slot.top > MIN_SLOT_SIZE) {
nextRects.push(this.addRect(layout.width, 0, Infinity, slot.top));
}
// If slot is not aligned to the bottom, create a new free space below
// the slot.
if (slot.top + slot.height < layout.height - MIN_SLOT_SIZE) {
nextRects.push(
this.addRect(
layout.width,
slot.top + slot.height,
Infinity,
layout.height - slot.top - slot.height
)
);
}
// Update layout width.
layout.width = slot.left + slot.width;
}
// Clean up the current free spaces making sure none of them overlap with
// the slot. Split all overlapping free spaces into smaller shards that do
// not overlap with the slot.
if (!ignoreCurrentRects) {
if (fillGaps) i = 0;
for (; i < currentRects.length; i++) {
rectId = currentRects[i];
if (!rectId) continue;
rect = this.getRect(rectId);
shards = this.splitRect(rect, slot);
for (j = 0; j < shards.length; j++) {
rectId = shards[j];
rect = this.getRect(rectId);
// Make sure that the free space is within the boundaries of the
// layout. This routine is critical to the algorithm as it makes sure
// that there are no leftover spaces with infinite height/width.
// It's also essential that we don't compare values absolutely to each
// other but leave a little headroom (EPSILON) to get rid of false
// positives.
if (
horizontal ? rect.left + EPS < layout.width - EPS : rect.top + EPS < layout.height - EPS
) {
nextRects.push(rectId);
}
}
}
}
// Sanitize and sort all the new free spaces that will be used in the next
// iteration. This procedure is critical to make the bin-packing algorithm
// work. The free spaces have to be in correct order in the beginning of the
// next iteration.
if (nextRects.length > 1) {
this.purgeRects(nextRects).sort(horizontal ? this.sortRectsLeftTop : this.sortRectsTopLeft);
}
// Finally we need to make sure that `this.currentRects` points to
// `nextRects` array as that is used in the next iteration's beginning when
// we try to find a space for the next slot.
this.currentRects = nextRects;
this.nextRects = currentRects;
return slot;
};
/**
* Add a new rectangle to the rectangle store. Returns the id of the new
* rectangle.
*
* @param {Number} left
* @param {Number} top
* @param {Number} width
* @param {Number} height
* @returns {Number}
*/
PackerProcessor.prototype.addRect = function (left, top, width, height) {
var rectId = ++this.rectId;
this.rectStore[rectId] = left || 0;
this.rectStore[++this.rectId] = top || 0;
this.rectStore[++this.rectId] = width || 0;
this.rectStore[++this.rectId] = height || 0;
return rectId;
};
/**
* Get rectangle data from the rectangle store by id. Optionally you can
* provide a target object where the rectangle data will be written in. By
* default an internal object is reused as a target object.
*
* @param {Number} id
* @param {Object} [target]
* @returns {Object}
*/
PackerProcessor.prototype.getRect = function (id, target) {
if (!target) target = this.rectTarget;
target.left = this.rectStore[id] || 0;
target.top = this.rectStore[++id] || 0;
target.width = this.rectStore[++id] || 0;
target.height = this.rectStore[++id] || 0;
return target;
};
/**
* Punch a hole into a rectangle and return the shards (1-4).
*
* @param {Object} rect
* @param {Object} hole
* @returns {Number[]}
*/
PackerProcessor.prototype.splitRect = (function () {
var shards = [];
var width = 0;
var height = 0;
return function (rect, hole) {
// Reset old shards.
shards.length = 0;
// If the slot does not overlap with the hole add slot to the return data
// as is. Note that in this case we are eager to keep the slot as is if
// possible so we use the EPSILON in favour of that logic.
if (
rect.left + rect.width <= hole.left + EPS ||
hole.left + hole.width <= rect.left + EPS ||
rect.top + rect.height <= hole.top + EPS ||
hole.top + hole.height <= rect.top + EPS
) {
shards.push(this.addRect(rect.left, rect.top, rect.width, rect.height));
return shards;
}
// Left split.
width = hole.left - rect.left;
if (width >= MIN_SLOT_SIZE) {
shards.push(this.addRect(rect.left, rect.top, width, rect.height));
}
// Right split.
width = rect.left + rect.width - (hole.left + hole.width);
if (width >= MIN_SLOT_SIZE) {
shards.push(this.addRect(hole.left + hole.width, rect.top, width, rect.height));
}
// Top split.
height = hole.top - rect.top;
if (height >= MIN_SLOT_SIZE) {
shards.push(this.addRect(rect.left, rect.top, rect.width, height));
}
// Bottom split.
height = rect.top + rect.height - (hole.top + hole.height);
if (height >= MIN_SLOT_SIZE) {
shards.push(this.addRect(rect.left, hole.top + hole.height, rect.width, height));
}
return shards;
};
})();
/**
* Check if a rectangle is fully within another rectangle.
*
* @param {Object} a
* @param {Object} b
* @returns {Boolean}
*/
PackerProcessor.prototype.isRectAWithinRectB = function (a, b) {
return (
a.left + EPS >= b.left &&
a.top + EPS >= b.top &&
a.left + a.width - EPS <= b.left + b.width &&
a.top + a.height - EPS <= b.top + b.height
);
};
/**
* Loops through an array of rectangle ids and resets all that are fully
* within another rectangle in the array. Resetting in this case means that
* the rectangle id value is replaced with zero.
*
* @param {Number[]} rectIds
* @returns {Number[]}
*/
PackerProcessor.prototype.purgeRects = (function () {
var rectA = {};
var rectB = {};
return function (rectIds) {
var i = rectIds.length;
var j;
while (i--) {
j = rectIds.length;
if (!rectIds[i]) continue;
this.getRect(rectIds[i], rectA);
while (j--) {
if (!rectIds[j] || i === j) continue;
this.getRect(rectIds[j], rectB);
if (this.isRectAWithinRectB(rectA, rectB)) {
rectIds[i] = 0;
break;
}
}
}
return rectIds;
};
})();
/**
* Sort rectangles with top-left gravity.
*
* @param {Number} aId
* @param {Number} bId
* @returns {Number}
*/
PackerProcessor.prototype.sortRectsTopLeft = (function () {
var rectA = {};
var rectB = {};
return function (aId, bId) {
this.getRect(aId, rectA);
this.getRect(bId, rectB);
return rectA.top < rectB.top && rectA.top + EPS < rectB.top
? -1
: rectA.top > rectB.top && rectA.top - EPS > rectB.top
? 1
: rectA.left < rectB.left && rectA.left + EPS < rectB.left
? -1
: rectA.left > rectB.left && rectA.left - EPS > rectB.left
? 1
: 0;
};
})();
/**
* Sort rectangles with left-top gravity.
*
* @param {Number} aId
* @param {Number} bId
* @returns {Number}
*/
PackerProcessor.prototype.sortRectsLeftTop = (function () {
var rectA = {};
var rectB = {};
return function (aId, bId) {
this.getRect(aId, rectA);
this.getRect(bId, rectB);
return rectA.left < rectB.left && rectA.left + EPS < rectB.left
? -1
: rectA.left > rectB.left && rectA.left - EPS < rectB.left
? 1
: rectA.top < rectB.top && rectA.top + EPS < rectB.top
? -1
: rectA.top > rectB.top && rectA.top - EPS > rectB.top
? 1
: 0;
};
})();
if (isWorker) {
var PACKET_INDEX_WIDTH = 1;
var PACKET_INDEX_HEIGHT = 2;
var PACKET_INDEX_OPTIONS = 3;
var PACKET_HEADER_SLOTS = 4;
var processor = new PackerProcessor();
self.onmessage = function (msg) {
var data = new Float32Array(msg.data);
var items = data.subarray(PACKET_HEADER_SLOTS, data.length);
var slots = new Float32Array(items.length);
var settings = data[PACKET_INDEX_OPTIONS];
var layout = {
items: items,
slots: slots,
width: data[PACKET_INDEX_WIDTH],
height: data[PACKET_INDEX_HEIGHT],
};
// Compute the layout (width / height / slots).
processor.computeLayout(layout, settings);
// Copy layout data to the return data.
data[PACKET_INDEX_WIDTH] = layout.width;
data[PACKET_INDEX_HEIGHT] = layout.height;
data.set(layout.slots, PACKET_HEADER_SLOTS);
// Send layout back to the main thread.
postMessage(data.buffer, [data.buffer]);
};
}
return PackerProcessor;
}
var PackerProcessor = createPackerProcessor();
export default PackerProcessor;
//
// WORKER UTILS
//
var blobUrl = null;
var activeWorkers = [];
export function createWorkerProcessors(amount, onmessage) {
var workers = [];
if (amount > 0) {
if (!blobUrl) {
blobUrl = URL.createObjectURL(
new Blob(['(' + createPackerProcessor.toString() + ')(true)'], {
type: 'application/javascript',
})
);
}
for (var i = 0, worker; i < amount; i++) {
worker = new Worker(blobUrl);
if (onmessage) worker.onmessage = onmessage;
workers.push(worker);
activeWorkers.push(worker);
}
}
return workers;
}
export function destroyWorkerProcessors(workers) {
var worker;
var index;
for (var i = 0; i < workers.length; i++) {
worker = workers[i];
worker.onmessage = null;
worker.onerror = null;
worker.onmessageerror = null;
worker.terminate();
index = activeWorkers.indexOf(worker);
if (index > -1) activeWorkers.splice(index, 1);
}
if (blobUrl && !activeWorkers.length) {
URL.revokeObjectURL(blobUrl);
blobUrl = null;
}
}
export function isWorkerProcessorsSupported() {
return !!(window.Worker && window.URL && window.Blob);
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2018, Niklas Rämö
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,94 @@
/**
* Muuri Ticker
* Copyright (c) 2018-present, Niklas Rämö <inramo@gmail.com>
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/src/Ticker/LICENSE.md
*/
import raf from '../utils/raf';
/**
* A ticker system for handling DOM reads and writes in an efficient way.
*
* @class
*/
function Ticker(numLanes) {
this._nextStep = null;
this._lanes = [];
this._stepQueue = [];
this._stepCallbacks = {};
this._step = this._step.bind(this);
for (var i = 0; i < numLanes; i++) {
this._lanes.push(new TickerLane());
}
}
Ticker.prototype._step = function (time) {
var lanes = this._lanes;
var stepQueue = this._stepQueue;
var stepCallbacks = this._stepCallbacks;
var i, j, id, laneQueue, laneCallbacks, laneIndices;
this._nextStep = null;
for (i = 0; i < lanes.length; i++) {
laneQueue = lanes[i].queue;
laneCallbacks = lanes[i].callbacks;
laneIndices = lanes[i].indices;
for (j = 0; j < laneQueue.length; j++) {
id = laneQueue[j];
if (!id) continue;
stepQueue.push(id);
stepCallbacks[id] = laneCallbacks[id];
delete laneCallbacks[id];
delete laneIndices[id];
}
laneQueue.length = 0;
}
for (i = 0; i < stepQueue.length; i++) {
id = stepQueue[i];
if (stepCallbacks[id]) stepCallbacks[id](time);
delete stepCallbacks[id];
}
stepQueue.length = 0;
};
Ticker.prototype.add = function (laneIndex, id, callback) {
this._lanes[laneIndex].add(id, callback);
if (!this._nextStep) this._nextStep = raf(this._step);
};
Ticker.prototype.remove = function (laneIndex, id) {
this._lanes[laneIndex].remove(id);
};
/**
* A lane for ticker.
*
* @class
*/
function TickerLane() {
this.queue = [];
this.indices = {};
this.callbacks = {};
}
TickerLane.prototype.add = function (id, callback) {
var index = this.indices[id];
if (index !== undefined) this.queue[index] = undefined;
this.queue.push(id);
this.callbacks[id] = callback;
this.indices[id] = this.queue.length - 1;
};
TickerLane.prototype.remove = function (id) {
var index = this.indices[id];
if (index === undefined) return;
this.queue[index] = undefined;
delete this.callbacks[id];
delete this.indices[id];
};
export default Ticker;

43
app/src/vendor/muuri-src/constants.js vendored Normal file
View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
export var GRID_INSTANCES = {};
export var ITEM_ELEMENT_MAP = typeof Map === 'function' ? new Map() : null;
export var ACTION_SWAP = 'swap';
export var ACTION_MOVE = 'move';
export var EVENT_SYNCHRONIZE = 'synchronize';
export var EVENT_LAYOUT_START = 'layoutStart';
export var EVENT_LAYOUT_END = 'layoutEnd';
export var EVENT_LAYOUT_ABORT = 'layoutAbort';
export var EVENT_ADD = 'add';
export var EVENT_REMOVE = 'remove';
export var EVENT_SHOW_START = 'showStart';
export var EVENT_SHOW_END = 'showEnd';
export var EVENT_HIDE_START = 'hideStart';
export var EVENT_HIDE_END = 'hideEnd';
export var EVENT_FILTER = 'filter';
export var EVENT_SORT = 'sort';
export var EVENT_MOVE = 'move';
export var EVENT_SEND = 'send';
export var EVENT_BEFORE_SEND = 'beforeSend';
export var EVENT_RECEIVE = 'receive';
export var EVENT_BEFORE_RECEIVE = 'beforeReceive';
export var EVENT_DRAG_INIT = 'dragInit';
export var EVENT_DRAG_START = 'dragStart';
export var EVENT_DRAG_MOVE = 'dragMove';
export var EVENT_DRAG_SCROLL = 'dragScroll';
export var EVENT_DRAG_END = 'dragEnd';
export var EVENT_DRAG_RELEASE_START = 'dragReleaseStart';
export var EVENT_DRAG_RELEASE_END = 'dragReleaseEnd';
export var EVENT_DESTROY = 'destroy';
export var HAS_TOUCH_EVENTS = 'ontouchstart' in window;
export var HAS_POINTER_EVENTS = !!window.PointerEvent;
export var HAS_MS_POINTER_EVENTS = !!window.navigator.msPointerEnabled;
export var MAX_SAFE_FLOAT32_INTEGER = 16777216;

596
app/src/vendor/muuri-src/index.d.ts vendored Normal file
View File

@ -0,0 +1,596 @@
export interface StyleDeclaration {
[styleProperty: string]: string;
}
export type EventListener = (...args: any[]) => any;
export interface DraggerCssProps {
touchAction?: string;
userSelect?: string;
userDrag?: string;
tapHighlightColor?: string;
touchCallout?: string;
contentZooming?: string;
}
export interface DraggerEvent {
type: 'start' | 'move' | 'end' | 'cancel';
srcEvent: PointerEvent | TouchEvent | MouseEvent;
distance: number;
deltaX: number;
deltaY: number;
deltaTime: number;
isFirst: boolean;
isFinal: boolean;
pointerType: 'mouse' | 'pen' | 'touch';
identifier: number;
screenX: number;
screenY: number;
clientX: number;
clientY: number;
pageX: number;
pageY: number;
target: HTMLElement;
}
export interface DraggerStartEvent extends DraggerEvent {
type: 'start';
distance: 0;
deltaX: 0;
deltaY: 0;
deltaTime: 0;
isFirst: true;
isFinal: false;
}
export interface DraggerMoveEvent extends DraggerEvent {
type: 'move';
isFirst: false;
isFinal: false;
}
export interface DraggerEndEvent extends DraggerEvent {
type: 'end';
isFirst: false;
isFinal: true;
}
export interface DraggerCancelEvent extends DraggerEvent {
type: 'cancel';
isFirst: false;
isFinal: true;
}
export interface DraggerEvents {
start(event: DraggerStartEvent): any;
move(event: DraggerMoveEvent): any;
end(event: DraggerMoveEvent): any;
cancel(event: DraggerCancelEvent): any;
}
export interface ScrollEvent extends Event {
type: 'scroll';
}
export interface GridEvents {
synchronize(): any;
layoutStart(items: Item[], isInstant: boolean): any;
layoutEnd(items: Item[]): any;
layoutAbort(items: Item[]): any;
add(items: Item[]): any;
remove(items: Item[], indices: number[]): any;
showStart(items: Item[]): any;
showEnd(items: Item[]): any;
hideStart(items: Item[]): any;
hideEnd(items: Item[]): any;
filter(shownItems: Item[], hiddenItems: Item[]): any;
sort(currentOrder: Item[], previousOrder: Item[]): any;
move(data: { item: Item; fromIndex: number; toIndex: number; action: 'move' | 'swap' }): any;
send(data: { item: Item; fromGrid: Grid; fromIndex: number; toGrid: Grid; toIndex: number }): any;
beforeSend(data: {
item: Item;
fromGrid: Grid;
fromIndex: number;
toGrid: Grid;
toIndex: number;
}): any;
receive(data: {
item: Item;
fromGrid: Grid;
fromIndex: number;
toGrid: Grid;
toIndex: number;
}): any;
beforeReceive(data: {
item: Item;
fromGrid: Grid;
fromIndex: number;
toGrid: Grid;
toIndex: number;
}): any;
dragInit(item: Item, event: DraggerStartEvent | DraggerMoveEvent): any;
dragStart(item: Item, event: DraggerStartEvent | DraggerMoveEvent): any;
dragMove(item: Item, event: DraggerMoveEvent): any;
dragScroll(item: Item, event: ScrollEvent): any;
dragEnd(item: Item, event: DraggerEndEvent | DraggerCancelEvent): any;
dragReleaseStart(item: Item): any;
dragReleaseEnd(item: Item): any;
destroy(): any;
}
export interface LayoutData {
id: number;
items: Item[];
slots: number[];
styles?: StyleDeclaration | null;
[key: string]: any;
}
export interface LayoutOptions {
fillGaps?: boolean;
horizontal?: boolean;
alignRight?: boolean;
alignBottom?: boolean;
rounding?: boolean;
}
export type LayoutOnFinish = (items: Item[], isAborted: boolean) => any;
export type LayoutFunctionCallback = (layout: LayoutData) => any;
export type LayoutFunctionCancel = (...args: any[]) => any;
export type LayoutFunction = (
grid: Grid,
id: number,
items: Item[],
gridWidth: number,
gridHeight: number,
callback: LayoutFunctionCallback
) => void | undefined | LayoutFunctionCancel;
export type SortDataGetter = (item: Item, element: HTMLElement) => any;
export type DragStartPredicate = (
item: Item,
event: DraggerStartEvent | DraggerMoveEvent | DraggerEndEvent | DraggerCancelEvent
) => boolean | undefined;
export interface DragStartPredicateOptions {
distance?: number;
delay?: number;
}
export type DragSortGetter = (this: Grid, item: Item) => Grid[] | null | void | undefined;
export interface DragSortHeuristics {
sortInterval?: number;
minDragDistance?: number;
minBounceBackAngle?: number;
}
export type DragSortPredicateResult = {
grid: Grid;
index: number;
action: 'move' | 'swap';
} | null;
export type DragSortPredicate = (item: Item, event: DraggerMoveEvent) => DragSortPredicateResult;
export interface DragSortPredicateOptions {
threshold?: number;
action?: 'move' | 'swap';
migrateAction?: 'move' | 'swap';
}
export interface DragReleaseOptions {
duration?: number;
easing?: string;
useDragContainer?: boolean;
}
export type DragPlaceholderCreateElement = (item: Item) => HTMLElement;
export type DragPlaceholderOnCreate = (item: Item, placeholderElement: HTMLElement) => any;
export type DragPlaceholderOnRemove = (item: Item, placeholderElement: HTMLElement) => any;
export interface DragPlaceholderOptions {
enabled?: boolean;
createElement?: DragPlaceholderCreateElement | null;
onCreate?: DragPlaceholderOnCreate | null;
onRemove?: DragPlaceholderOnRemove | null;
}
export interface DragAutoScrollTarget {
element: Window | HTMLElement;
axis?: number;
priority?: number;
threshold?: number;
}
export type DragAutoScrollTargets = Array<Window | HTMLElement | DragAutoScrollTarget>;
export type DragAutoScrollTargetsGetter = (item: Item) => DragAutoScrollTargets;
export type DragAutoScrollOnStart = (
item: Item,
scrollElement: Window | HTMLElement,
scrollDirection: number
) => any;
export type DragAutoScrollOnStop = (
item: Item,
scrollElement: Window | HTMLElement,
scrollDirection: number
) => any;
export type DragAutoScrollHandle = (
item: Item,
itemClientX: number,
itemClientY: number,
itemWidth: number,
itemHeight: number,
pointerClientX: number,
pointerClientY: number
) => {
left: number;
top: number;
width: number;
height: number;
};
export type DragAutoScrollSpeed = (
item: Item,
scrollElement: Window | HTMLElement,
scrollData: {
direction: number;
threshold: number;
distance: number;
value: number;
maxValue: number;
duration: number;
speed: number;
deltaTime: number;
isEnding: boolean;
}
) => number;
export interface DragAutoScrollOptions {
targets?: DragAutoScrollTargets | DragAutoScrollTargetsGetter;
handle?: DragAutoScrollHandle | null;
threshold?: number;
safeZone?: number;
speed?: number | DragAutoScrollSpeed;
sortDuringScroll?: boolean;
smoothStop?: boolean;
onStart?: DragAutoScrollOnStart | null;
onStop?: DragAutoScrollOnStop | null;
}
export interface GridOptions {
items?: HTMLElement[] | NodeList | HTMLCollection | string;
showDuration?: number;
showEasing?: string;
visibleStyles?: StyleDeclaration;
hideDuration?: number;
hideEasing?: string;
hiddenStyles?: StyleDeclaration;
layout?: LayoutOptions | LayoutFunction;
layoutOnResize?: boolean | number;
layoutOnInit?: boolean;
layoutDuration?: number;
layoutEasing?: string;
sortData?: { [key: string]: SortDataGetter } | null;
dragEnabled?: boolean;
dragHandle?: string | null;
dragContainer?: HTMLElement | null;
dragStartPredicate?: DragStartPredicateOptions | DragStartPredicate;
dragAxis?: 'x' | 'y' | 'xy';
dragSort?: boolean | DragSortGetter;
dragSortLock?: ((item: Item, event: DraggerEvent | null) => boolean) | null;
dragSortHeuristics?: DragSortHeuristics;
dragSortPredicate?: DragSortPredicateOptions | DragSortPredicate;
dragRelease?: DragReleaseOptions;
dragCssProps?: DraggerCssProps;
dragPlaceholder?: DragPlaceholderOptions;
dragAutoScroll?: DragAutoScrollOptions;
containerClass?: string;
itemClass?: string;
itemVisibleClass?: string;
itemHiddenClass?: string;
itemPositioningClass?: string;
itemDraggingClass?: string;
itemReleasingClass?: string;
itemPlaceholderClass?: string;
}
//
// CLASSES
//
export class Item {
constructor(grid: Grid, element: HTMLElement, isActive?: boolean);
getGrid(): Grid | undefined;
getElement(): HTMLElement | undefined;
getWidth(): number;
getHeight(): number;
getMargin(): { left: number; right: number; top: number; bottom: number };
getPosition(): { left: number; top: number };
isActive(): boolean;
isVisible(): boolean;
isShowing(): boolean;
isHiding(): boolean;
isPositioning(): boolean;
isDragging(): boolean;
isReleasing(): boolean;
isDestroyed(): boolean;
}
export class ItemLayout {
constructor(item: Item);
start(instant: boolean, onFinish?: (isInterrupted: boolean, item: Item) => any): void;
stop(processCallbackQueue: boolean, targetStyles?: StyleDeclaration): void;
destroy(): void;
}
export class ItemVisibility {
constructor(item: Item);
show(instant: boolean, onFinish?: (isInterrupted: boolean, item: Item) => any): void;
hide(instant: boolean, onFinish?: (isInterrupted: boolean, item: Item) => any): void;
stop(processCallbackQueue: boolean, applyCurrentStyles?: boolean): void;
setStyles(styles: StyleDeclaration): void;
destroy(): void;
}
export class ItemMigrate {
constructor(item: Item);
start(targetGrid: Grid, position: HTMLElement | number | Item, container?: HTMLElement): void;
stop(abort?: boolean, left?: number, top?: number): void;
destroy(): void;
}
export class ItemDrag {
constructor(item: Item);
static autoScroller: AutoScroller;
static defaultStartPredicate(
item: Item,
event: DraggerEvent,
options?: DragStartPredicateOptions
): boolean | undefined;
static defaultSortPredicate(
item: Item,
options?: DragSortPredicateOptions
): DragSortPredicateResult;
stop(): void;
sort(force?: boolean): void;
destroy(): void;
}
export class ItemDragRelease {
constructor(item: Item);
start(): void;
stop(abort?: boolean, left?: number, top?: number): void;
isJustReleased(): boolean;
destroy(): void;
}
export class ItemDragPlaceholder {
constructor(item: Item);
create(): void;
reset(): void;
isActive(): boolean;
getElement(): HTMLElement | null;
updateDimensions(): void;
destroy(): void;
}
export class Emitter {
constructor();
on(event: string, listener: EventListener): this;
off(event: string, listener: EventListener): this;
clear(event: string): this;
emit(event: string, ...args: any[]): this;
burst(event: string, ...args: any[]): this;
countListeners(event: string): number;
destroy(): this;
}
export class Animator {
constructor(element: HTMLElement);
start(
propsFrom: StyleDeclaration,
propsTo: StyleDeclaration,
options?: {
duration?: number;
easing?: string;
onFinish?: (...args: any[]) => any;
}
): void;
stop(applyCurrentStyles?: boolean): void;
isAnimating(): boolean;
destroy(): void;
}
export class Dragger {
constructor(element: HTMLElement, cssProps?: DraggerCssProps);
isActive(): boolean;
setTouchAction(touchAction: string): void;
setCssProps(props?: DraggerCssProps): void;
getDeltaX(): number;
getDeltaY(): number;
getDistance(): number;
getDeltaTime(): number;
on<T extends keyof DraggerEvents>(event: T, listener: DraggerEvents[T]): void;
off<T extends keyof DraggerEvents>(event: T, listener: DraggerEvents[T]): void;
destroy(): void;
}
export class AutoScroller {
constructor();
static AXIS_X: 1;
static AXIS_Y: 2;
static FORWARD: 4;
static BACKWARD: 8;
static LEFT: 9;
static RIGHT: 5;
static UP: 10;
static DOWN: 6;
static smoothSpeed(
maxSpeed: number,
acceleration: number,
deceleration: number
): DragAutoScrollSpeed;
static pointerHandle(pointerSize: number): DragAutoScrollHandle;
addItem(item: Item): void;
updateItem(item: Item): void;
removeItem(item: Item): void;
isItemScrollingX(item: Item): boolean;
isItemScrollingY(item: Item): boolean;
isItemScrolling(item: Item): boolean;
destroy(): void;
}
export class Packer {
constructor(numWorkers?: number, options?: LayoutOptions);
setOptions(options?: LayoutOptions): void;
createLayout(
grid: Grid,
id: number,
items: Item[],
width: number,
height: number,
callback: LayoutFunctionCallback
): LayoutFunctionCancel | void;
cancelLayout(id: number): void;
destroy(): void;
}
export default class Grid {
constructor(element: string | HTMLElement, options?: GridOptions);
static Item: typeof Item;
static ItemLayout: typeof ItemLayout;
static ItemVisibility: typeof ItemVisibility;
static ItemMigrate: typeof ItemMigrate;
static ItemDrag: typeof ItemDrag;
static ItemDragRelease: typeof ItemDragRelease;
static ItemDragPlaceholder: typeof ItemDragPlaceholder;
static Emitter: typeof Emitter;
static Animator: typeof Animator;
static Dragger: typeof Dragger;
static Packer: typeof Packer;
static AutoScroller: typeof AutoScroller;
static defaultPacker: Packer;
static defaultOptions: GridOptions;
on<T extends keyof GridEvents>(event: T, listener: GridEvents[T]): this;
off<T extends keyof GridEvents>(event: T, listener: GridEvents[T]): this;
getElement(): HTMLElement;
getItem(target: HTMLElement | number | Item): Item | null;
getItems(targets?: HTMLElement | number | Item | Array<HTMLElement | number | Item>): Item[];
refreshItems(items?: Item[], force?: boolean): this;
refreshSortData(items?: Item[]): this;
synchronize(): this;
layout(instant?: boolean, onFinish?: LayoutOnFinish): this;
add(
elements: HTMLElement | HTMLElement[] | NodeList | HTMLCollection,
options?: {
index?: number;
active?: boolean;
layout?: boolean | 'instant' | LayoutOnFinish;
}
): Item[];
remove(
items: Item[],
options?: {
removeElements?: boolean;
layout?: boolean | 'instant' | LayoutOnFinish;
}
): Item[];
show(
items: Item[],
options?: {
instant?: boolean;
syncWithLayout?: boolean;
onFinish?: (items: Item[]) => any;
layout?: boolean | 'instant' | LayoutOnFinish;
}
): this;
hide(
items: Item[],
options?: {
instant?: boolean;
syncWithLayout?: boolean;
onFinish?: (items: Item[]) => any;
layout?: boolean | 'instant' | LayoutOnFinish;
}
): this;
filter(
predicate: string | ((item: Item) => boolean),
options?: {
instant?: boolean;
syncWithLayout?: boolean;
onFinish?: (items: Item[]) => any;
layout?: boolean | 'instant' | LayoutOnFinish;
}
): this;
sort(
comparer: ((a: Item, b: Item) => number) | string | Item[],
options?: {
descending?: boolean;
layout?: boolean | 'instant' | LayoutOnFinish;
}
): this;
move(
item: HTMLElement | number | Item,
position: HTMLElement | number | Item,
options?: {
action?: 'move' | 'swap';
layout?: boolean | 'instant' | LayoutOnFinish;
}
): this;
send(
item: HTMLElement | number | Item,
targetGrid: Grid,
position: HTMLElement | number | Item,
options?: {
appendTo?: HTMLElement;
layoutSender?: boolean | 'instant' | LayoutOnFinish;
layoutReceiver?: boolean | 'instant' | LayoutOnFinish;
}
): this;
destroy(removeElements?: boolean): this;
}
export as namespace Muuri;

7
app/src/vendor/muuri-src/index.js vendored Normal file
View File

@ -0,0 +1,7 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
export { default } from './Grid/Grid';

126
app/src/vendor/muuri-src/ticker.js vendored Normal file
View File

@ -0,0 +1,126 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import Ticker from './Ticker/Ticker';
var LAYOUT_READ = 'layoutRead';
var LAYOUT_WRITE = 'layoutWrite';
var VISIBILITY_READ = 'visibilityRead';
var VISIBILITY_WRITE = 'visibilityWrite';
var DRAG_START_READ = 'dragStartRead';
var DRAG_START_WRITE = 'dragStartWrite';
var DRAG_MOVE_READ = 'dragMoveRead';
var DRAG_MOVE_WRITE = 'dragMoveWrite';
var DRAG_SCROLL_READ = 'dragScrollRead';
var DRAG_SCROLL_WRITE = 'dragScrollWrite';
var DRAG_SORT_READ = 'dragSortRead';
var PLACEHOLDER_LAYOUT_READ = 'placeholderLayoutRead';
var PLACEHOLDER_LAYOUT_WRITE = 'placeholderLayoutWrite';
var PLACEHOLDER_RESIZE_WRITE = 'placeholderResizeWrite';
var AUTO_SCROLL_READ = 'autoScrollRead';
var AUTO_SCROLL_WRITE = 'autoScrollWrite';
var DEBOUNCE_READ = 'debounceRead';
var LANE_READ = 0;
var LANE_READ_TAIL = 1;
var LANE_WRITE = 2;
var ticker = new Ticker(3);
export default ticker;
export function addLayoutTick(itemId, read, write) {
ticker.add(LANE_READ, LAYOUT_READ + itemId, read);
ticker.add(LANE_WRITE, LAYOUT_WRITE + itemId, write);
}
export function cancelLayoutTick(itemId) {
ticker.remove(LANE_READ, LAYOUT_READ + itemId);
ticker.remove(LANE_WRITE, LAYOUT_WRITE + itemId);
}
export function addVisibilityTick(itemId, read, write) {
ticker.add(LANE_READ, VISIBILITY_READ + itemId, read);
ticker.add(LANE_WRITE, VISIBILITY_WRITE + itemId, write);
}
export function cancelVisibilityTick(itemId) {
ticker.remove(LANE_READ, VISIBILITY_READ + itemId);
ticker.remove(LANE_WRITE, VISIBILITY_WRITE + itemId);
}
export function addDragStartTick(itemId, read, write) {
ticker.add(LANE_READ, DRAG_START_READ + itemId, read);
ticker.add(LANE_WRITE, DRAG_START_WRITE + itemId, write);
}
export function cancelDragStartTick(itemId) {
ticker.remove(LANE_READ, DRAG_START_READ + itemId);
ticker.remove(LANE_WRITE, DRAG_START_WRITE + itemId);
}
export function addDragMoveTick(itemId, read, write) {
ticker.add(LANE_READ, DRAG_MOVE_READ + itemId, read);
ticker.add(LANE_WRITE, DRAG_MOVE_WRITE + itemId, write);
}
export function cancelDragMoveTick(itemId) {
ticker.remove(LANE_READ, DRAG_MOVE_READ + itemId);
ticker.remove(LANE_WRITE, DRAG_MOVE_WRITE + itemId);
}
export function addDragScrollTick(itemId, read, write) {
ticker.add(LANE_READ, DRAG_SCROLL_READ + itemId, read);
ticker.add(LANE_WRITE, DRAG_SCROLL_WRITE + itemId, write);
}
export function cancelDragScrollTick(itemId) {
ticker.remove(LANE_READ, DRAG_SCROLL_READ + itemId);
ticker.remove(LANE_WRITE, DRAG_SCROLL_WRITE + itemId);
}
export function addDragSortTick(itemId, read) {
ticker.add(LANE_READ_TAIL, DRAG_SORT_READ + itemId, read);
}
export function cancelDragSortTick(itemId) {
ticker.remove(LANE_READ_TAIL, DRAG_SORT_READ + itemId);
}
export function addPlaceholderLayoutTick(itemId, read, write) {
ticker.add(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId, read);
ticker.add(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId, write);
}
export function cancelPlaceholderLayoutTick(itemId) {
ticker.remove(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId);
ticker.remove(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId);
}
export function addPlaceholderResizeTick(itemId, write) {
ticker.add(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId, write);
}
export function cancelPlaceholderResizeTick(itemId) {
ticker.remove(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId);
}
export function addAutoScrollTick(read, write) {
ticker.add(LANE_READ, AUTO_SCROLL_READ, read);
ticker.add(LANE_WRITE, AUTO_SCROLL_WRITE, write);
}
export function cancelAutoScrollTick() {
ticker.remove(LANE_READ, AUTO_SCROLL_READ);
ticker.remove(LANE_WRITE, AUTO_SCROLL_WRITE);
}
export function addDebounceTick(debounceId, read) {
ticker.add(LANE_READ, DEBOUNCE_READ + debounceId, read);
}
export function cancelDebounceTick(debounceId) {
ticker.remove(LANE_READ, DEBOUNCE_READ + debounceId);
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import elementMatches from './elementMatches';
/**
* Add class to an element.
*
* @param {HTMLElement} element
* @param {String} className
*/
export default function addClass(element, className) {
if (!className) return;
if (element.classList) {
element.classList.add(className);
} else {
if (!elementMatches(element, '.' + className)) {
element.className += ' ' + className;
}
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var tempArray = [];
var numberType = 'number';
/**
* Insert an item or an array of items to array to a specified index. Mutates
* the array. The index can be negative in which case the items will be added
* to the end of the array.
*
* @param {Array} array
* @param {*} items
* @param {Number} [index=-1]
*/
export default function arrayInsert(array, items, index) {
var startIndex = typeof index === numberType ? index : -1;
if (startIndex < 0) startIndex = array.length - startIndex + 1;
array.splice.apply(array, tempArray.concat(startIndex, 0, items));
tempArray.length = 0;
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import normalizeArrayIndex from './normalizeArrayIndex';
/**
* Move array item to another index.
*
* @param {Array} array
* @param {Number} fromIndex
* - Index (positive or negative) of the item that will be moved.
* @param {Number} toIndex
* - Index (positive or negative) where the item should be moved to.
*/
export default function arrayMove(array, fromIndex, toIndex) {
// Make sure the array has two or more items.
if (array.length < 2) return;
// Normalize the indices.
var from = normalizeArrayIndex(array, fromIndex);
var to = normalizeArrayIndex(array, toIndex);
// Add target item to the new position.
if (from !== to) {
array.splice(to, 0, array.splice(from, 1)[0]);
}
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import normalizeArrayIndex from './normalizeArrayIndex';
/**
* Swap array items.
*
* @param {Array} array
* @param {Number} index
* - Index (positive or negative) of the item that will be swapped.
* @param {Number} withIndex
* - Index (positive or negative) of the other item that will be swapped.
*/
export default function arraySwap(array, index, withIndex) {
// Make sure the array has two or more items.
if (array.length < 2) return;
// Normalize the indices.
var indexA = normalizeArrayIndex(array, index);
var indexB = normalizeArrayIndex(array, withIndex);
var temp;
// Swap the items.
if (indexA !== indexB) {
temp = array[indexA];
array[indexA] = array[indexB];
array[indexB] = temp;
}
}

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var id = 0;
/**
* Returns a unique numeric id (increments a base value on every call).
* @returns {Number}
*/
export default function createUid() {
return ++id;
}

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import { addDebounceTick, cancelDebounceTick } from '../ticker';
var debounceId = 0;
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. The returned function accepts one argument which, when
* being `true`, cancels the debounce function immediately. When the debounce
* function is canceled it cannot be invoked again.
*
* @param {Function} fn
* @param {Number} durationMs
* @returns {Function}
*/
export default function debounce(fn, durationMs) {
var id = ++debounceId;
var timer = 0;
var lastTime = 0;
var isCanceled = false;
var tick = function (time) {
if (isCanceled) return;
if (lastTime) timer -= time - lastTime;
lastTime = time;
if (timer > 0) {
addDebounceTick(id, tick);
} else {
timer = lastTime = 0;
fn();
}
};
return function (cancel) {
if (isCanceled) return;
if (durationMs <= 0) {
if (cancel !== true) fn();
return;
}
if (cancel === true) {
isCanceled = true;
timer = lastTime = 0;
tick = undefined;
cancelDebounceTick(id);
return;
}
if (timer <= 0) {
timer = durationMs;
tick(0);
} else {
timer = durationMs;
}
};
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var ElProto = window.Element.prototype;
var matchesFn =
ElProto.matches ||
ElProto.matchesSelector ||
ElProto.webkitMatchesSelector ||
ElProto.mozMatchesSelector ||
ElProto.msMatchesSelector ||
ElProto.oMatchesSelector ||
function () {
return false;
};
/**
* Check if element matches a CSS selector.
*
* @param {Element} el
* @param {String} selector
* @returns {Boolean}
*/
export default function elementMatches(el, selector) {
return matchesFn.call(el, selector);
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getStyle from './getStyle';
import isTransformed from './isTransformed';
/**
* Returns an absolute positioned element's containing block, which is
* considered to be the closest ancestor element that the target element's
* positioning is relative to. Disclaimer: this only works as intended for
* absolute positioned elements.
*
* @param {HTMLElement} element
* @returns {(Document|Element)}
*/
export default function getContainingBlock(element) {
// As long as the containing block is an element, static and not
// transformed, try to get the element's parent element and fallback to
// document. https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L339
var doc = document;
var res = element || doc;
while (res && res !== doc && getStyle(res, 'position') === 'static' && !isTransformed(res)) {
res = res.parentElement || doc;
}
return res;
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getStyle from './getStyle';
import getStyleName from './getStyleName';
/**
* Get current values of the provided styles definition object or array.
*
* @param {HTMLElement} element
* @param {(Object|Array} styles
* @return {Object}
*/
export default function getCurrentStyles(element, styles) {
var result = {};
var prop, i;
if (Array.isArray(styles)) {
for (i = 0; i < styles.length; i++) {
prop = styles[i];
result[prop] = getStyle(element, getStyleName(prop));
}
} else {
for (prop in styles) {
result[prop] = getStyle(element, getStyleName(prop));
}
}
return result;
}

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import isOverlapping from './isOverlapping';
/**
* Calculate intersection area between two rectangle.
*
* @param {Object} a
* @param {Object} b
* @returns {Number}
*/
export default function getIntersectionArea(a, b) {
if (!isOverlapping(a, b)) return 0;
var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left);
var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
return width * height;
}

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getIntersectionArea from './getIntersectionArea';
/**
* Calculate how many percent the intersection area of two rectangles is from
* the maximum potential intersection area between the rectangles.
*
* @param {Object} a
* @param {Object} b
* @returns {Number}
*/
export default function getIntersectionScore(a, b) {
var area = getIntersectionArea(a, b);
if (!area) return 0;
var maxArea = Math.min(a.width, b.width) * Math.min(a.height, b.height);
return (area / maxArea) * 100;
}

View File

@ -0,0 +1,90 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getContainingBlock from './getContainingBlock';
import getStyleAsFloat from './getStyleAsFloat';
var offsetA = {};
var offsetB = {};
var offsetDiff = {};
/**
* Returns the element's document offset, which in practice means the vertical
* and horizontal distance between the element's northwest corner and the
* document's northwest corner. Note that this function always returns the same
* object so be sure to read the data from it instead using it as a reference.
*
* @param {(Document|Element|Window)} element
* @param {Object} [offsetData]
* - Optional data object where the offset data will be inserted to. If not
* provided a new object will be created for the return data.
* @returns {Object}
*/
function getOffset(element, offsetData) {
var offset = offsetData || {};
var rect;
// Set up return data.
offset.left = 0;
offset.top = 0;
// Document's offsets are always 0.
if (element === document) return offset;
// Add viewport scroll left/top to the respective offsets.
offset.left = window.pageXOffset || 0;
offset.top = window.pageYOffset || 0;
// Window's offsets are the viewport scroll left/top values.
if (element.self === window.self) return offset;
// Add element's client rects to the offsets.
rect = element.getBoundingClientRect();
offset.left += rect.left;
offset.top += rect.top;
// Exclude element's borders from the offset.
offset.left += getStyleAsFloat(element, 'border-left-width');
offset.top += getStyleAsFloat(element, 'border-top-width');
return offset;
}
/**
* Calculate the offset difference two elements.
*
* @param {HTMLElement} elemA
* @param {HTMLElement} elemB
* @param {Boolean} [compareContainingBlocks=false]
* - When this is set to true the containing blocks of the provided elements
* will be used for calculating the difference. Otherwise the provided
* elements will be compared directly.
* @returns {Object}
*/
export default function getOffsetDiff(elemA, elemB, compareContainingBlocks) {
offsetDiff.left = 0;
offsetDiff.top = 0;
// If elements are same let's return early.
if (elemA === elemB) return offsetDiff;
// Compare containing blocks if necessary.
if (compareContainingBlocks) {
elemA = getContainingBlock(elemA);
elemB = getContainingBlock(elemB);
// If containing blocks are identical, let's return early.
if (elemA === elemB) return offsetDiff;
}
// Finally, let's calculate the offset diff.
getOffset(elemA, offsetA);
getOffset(elemB, offsetB);
offsetDiff.left = offsetB.left - offsetA.left;
offsetDiff.top = offsetB.top - offsetA.top;
return offsetDiff;
}

View File

@ -0,0 +1,34 @@
/**
* Forked from hammer.js:
* https://github.com/hammerjs/hammer.js/blob/563b5b1e4bfbb5796798dd286cd57b7c56f1eb9e/src/utils/prefixed.js
*/
// Playing it safe here, test all potential prefixes capitalized and lowercase.
var vendorPrefixes = ['', 'webkit', 'moz', 'ms', 'o', 'Webkit', 'Moz', 'MS', 'O'];
var cache = {};
/**
* Get prefixed CSS property name when given a non-prefixed CSS property name.
* Returns null if the property is not supported at all.
*
* @param {CSSStyleDeclaration} style
* @param {String} prop
* @returns {String}
*/
export default function getPrefixedPropName(style, prop) {
var prefixedProp = cache[prop] || '';
if (prefixedProp) return prefixedProp;
var camelProp = prop[0].toUpperCase() + prop.slice(1);
var i = 0;
while (i < vendorPrefixes.length) {
prefixedProp = vendorPrefixes[i] ? vendorPrefixes[i] + camelProp : prop;
if (prefixedProp in style) {
cache[prop] = prefixedProp;
return prefixedProp;
}
++i;
}
return '';
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import isScrollable from './isScrollable';
/**
* Collect element's ancestors that are potentially scrollable elements. The
* provided element is also also included in the check, meaning that if it is
* scrollable it is added to the result array.
*
* @param {HTMLElement} element
* @param {Array} [result]
* @returns {Array}
*/
export default function getScrollableAncestors(element, result) {
result = result || [];
// Find scroll parents.
while (element && element !== document) {
// If element is inside ShadowDOM let's get it's host node from the real
// DOM and continue looping.
if (element.getRootNode && element instanceof DocumentFragment) {
element = element.getRootNode().host;
continue;
}
// If element is scrollable let's add it to the scrollable list.
if (isScrollable(element)) {
result.push(element);
}
element = element.parentNode;
}
// Always add window to the results.
result.push(window);
return result;
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var cache = typeof WeakMap === 'function' ? new WeakMap() : null;
/**
* Returns the computed value of an element's style property as a string.
*
* @param {HTMLElement} element
* @param {String} style
* @returns {String}
*/
export default function getStyle(element, style) {
var styles = cache && cache.get(element);
if (!styles) {
styles = window.getComputedStyle(element, null);
if (cache) cache.set(element, styles);
}
return styles.getPropertyValue(style);
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getStyle from './getStyle';
/**
* Returns the computed value of an element's style property transformed into
* a float value.
*
* @param {HTMLElement} el
* @param {String} style
* @returns {Number}
*/
export default function getStyleAsFloat(el, style) {
return parseFloat(getStyle(el, style)) || 0;
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var styleNameRegEx = /([A-Z])/g;
var prefixRegex = /^(webkit-|moz-|ms-|o-)/;
var msPrefixRegex = /^(-m-s-)/;
/**
* Transforms a camel case style property to kebab case style property. Handles
* vendor prefixed properties elegantly as well, e.g. "WebkitTransform" and
* "webkitTransform" are both transformed into "-webkit-transform".
*
* @param {String} property
* @returns {String}
*/
export default function getStyleName(property) {
// Initial slicing, turns "fooBarProp" into "foo-bar-prop".
var styleName = property.replace(styleNameRegEx, '-$1').toLowerCase();
// Handle properties that start with "webkit", "moz", "ms" or "o" prefix (we
// need to add an extra '-' to the beginnig).
styleName = styleName.replace(prefixRegex, '-$1');
// Handle properties that start with "MS" prefix (we need to transform the
// "-m-s-" into "-ms-").
styleName = styleName.replace(msPrefixRegex, '-ms-');
return styleName;
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getStyle from './getStyle';
import transformStyle from './transformStyle';
var translateValue = {};
var transformNone = 'none';
var rxMat3d = /^matrix3d/;
var rxMatTx = /([^,]*,){4}/;
var rxMat3dTx = /([^,]*,){12}/;
var rxNextItem = /[^,]*,/;
/**
* Returns the element's computed translateX and translateY values as a floats.
* The returned object is always the same object and updated every time this
* function is called.
*
* @param {HTMLElement} element
* @returns {Object}
*/
export default function getTranslate(element) {
translateValue.x = 0;
translateValue.y = 0;
var transform = getStyle(element, transformStyle);
if (!transform || transform === transformNone) {
return translateValue;
}
// Transform style can be in either matrix3d(...) or matrix(...).
var isMat3d = rxMat3d.test(transform);
var tX = transform.replace(isMat3d ? rxMat3dTx : rxMatTx, '');
var tY = tX.replace(rxNextItem, '');
translateValue.x = parseFloat(tX) || 0;
translateValue.y = parseFloat(tY) || 0;
return translateValue;
}

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
/**
* Transform translateX and translateY value into CSS transform style
* property's value.
*
* @param {Number} x
* @param {Number} y
* @returns {String}
*/
export default function getTranslateString(x, y) {
return 'translateX(' + x + 'px) translateY(' + y + 'px)';
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var unprefixRegEx = /^(webkit|moz|ms|o|Webkit|Moz|MS|O)(?=[A-Z])/;
var cache = {};
/**
* Remove any potential vendor prefixes from a property name.
*
* @param {String} prop
* @returns {String}
*/
export default function getUnprefixedPropName(prop) {
var result = cache[prop];
if (result) return result;
result = prop.replace(unprefixRegEx, '');
if (result !== prop) {
result = result[0].toLowerCase() + result.slice(1);
}
cache[prop] = result;
return result;
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
/**
* Check if passive events are supported.
* https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection
*
* @returns {Boolean}
*/
export default function hasPassiveEvents() {
var isPassiveEventsSupported = false;
try {
var passiveOpts = Object.defineProperty({}, 'passive', {
get: function () {
isPassiveEventsSupported = true;
},
});
window.addEventListener('testPassive', null, passiveOpts);
window.removeEventListener('testPassive', null, passiveOpts);
} catch (e) {}
return isPassiveEventsSupported;
}

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var functionType = 'function';
/**
* Check if a value is a function.
*
* @param {*} val
* @returns {Boolean}
*/
export default function isFunction(val) {
return typeof val === functionType;
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import isFunction from './isFunction';
var nativeCode = '[native code]';
/**
* Check if a value (e.g. a method or constructor) is native code. Good for
* detecting when a polyfill is used and when not.
*
* @param {*} feat
* @returns {Boolean}
*/
export default function isNative(feat) {
var S = window.Symbol;
return !!(
feat &&
isFunction(S) &&
isFunction(S.toString) &&
S(feat).toString().indexOf(nativeCode) > -1
);
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var htmlCollectionType = '[object HTMLCollection]';
var nodeListType = '[object NodeList]';
/**
* Check if a value is a node list or a html collection.
*
* @param {*} val
* @returns {Boolean}
*/
export default function isNodeList(val) {
var type = Object.prototype.toString.call(val);
return type === htmlCollectionType || type === nodeListType;
}

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
/**
* Check if two rectangles are overlapping.
*
* @param {Object} a
* @param {Object} b
* @returns {Number}
*/
export default function isOverlapping(a, b) {
return !(
a.left + a.width <= b.left ||
b.left + b.width <= a.left ||
a.top + a.height <= b.top ||
b.top + b.height <= a.top
);
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var objectType = 'object';
var objectToStringType = '[object Object]';
var toString = Object.prototype.toString;
/**
* Check if a value is a plain object.
*
* @param {*} val
* @returns {Boolean}
*/
export default function isPlainObject(val) {
return typeof val === objectType && toString.call(val) === objectToStringType;
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getStyle from './getStyle';
/**
* Check if overflow style value is scrollable.
*
* @param {String} value
* @returns {Boolean}
*/
function isScrollableOverflow(value) {
return value === 'auto' || value === 'scroll' || value === 'overlay';
}
/**
* Check if an element is scrollable.
*
* @param {HTMLElement} element
* @returns {Boolean}
*/
export default function isScrollable(element) {
return (
isScrollableOverflow(getStyle(element, 'overflow')) ||
isScrollableOverflow(getStyle(element, 'overflow-x')) ||
isScrollableOverflow(getStyle(element, 'overflow-y'))
);
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getStyle from './getStyle';
import transformStyle from './transformStyle';
var transformNone = 'none';
var displayInline = 'inline';
var displayNone = 'none';
var displayStyle = 'display';
/**
* Returns true if element is transformed, false if not. In practice the
* element's display value must be anything else than "none" or "inline" as
* well as have a valid transform value applied in order to be counted as a
* transformed element.
*
* Borrowed from Mezr (v0.6.1):
* https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L661
*
* @param {HTMLElement} element
* @returns {Boolean}
*/
export default function isTransformed(element) {
var transform = getStyle(element, transformStyle);
if (!transform || transform === transformNone) return false;
var display = getStyle(element, displayStyle);
if (display === displayInline || display === displayNone) return false;
return true;
}

View File

@ -0,0 +1,7 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
export default function noop() {}

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
/**
* Normalize array index. Basically this function makes sure that the provided
* array index is within the bounds of the provided array and also transforms
* negative index to the matching positive index. The third (optional) argument
* allows you to define offset for array's length in case you are adding items
* to the array or removing items from the array.
*
* @param {Array} array
* @param {Number} index
* @param {Number} [sizeOffset]
*/
export default function normalizeArrayIndex(array, index, sizeOffset) {
var maxIndex = Math.max(0, array.length - 1 + (sizeOffset || 0));
return index > maxIndex ? maxIndex : index < 0 ? Math.max(maxIndex + index + 1, 0) : index;
}

25
app/src/vendor/muuri-src/utils/raf.js vendored Normal file
View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
var dt = 1000 / 60;
var raf = (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
return this.setTimeout(function () {
callback(Date.now());
}, dt);
}
).bind(window);
/**
* @param {Function} callback
* @returns {Number}
*/
export default raf;

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import elementMatches from './elementMatches';
/**
* Remove class from an element.
*
* @param {HTMLElement} element
* @param {String} className
*/
export default function removeClass(element, className) {
if (!className) return;
if (element.classList) {
element.classList.remove(className);
} else {
if (elementMatches(element, '.' + className)) {
element.className = (' ' + element.className + ' ')
.replace(' ' + className + ' ', ' ')
.trim();
}
}
}

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
/**
* Set inline styles to an element.
*
* @param {HTMLElement} element
* @param {Object} styles
*/
export default function setStyles(element, styles) {
for (var prop in styles) {
element.style[prop] = styles[prop];
}
}

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import isNodeList from './isNodeList';
/**
* Converts a value to an array or clones an array.
*
* @param {*} val
* @returns {Array}
*/
export default function toArray(val) {
return isNodeList(val) ? Array.prototype.slice.call(val) : Array.prototype.concat(val);
}

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import getPrefixedPropName from './getPrefixedPropName';
var transformProp = getPrefixedPropName(document.documentElement.style, 'transform') || 'transform';
export default transformProp;

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import transformProp from './transformProp';
import getStyleName from './getStyleName';
var transformStyle = getStyleName(transformProp);
export default transformStyle;

View File

@ -8,6 +8,7 @@ export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'muuri': path.resolve(__dirname, './src/vendor/muuri-src/index.js'),
},
},
})