Created
July 17, 2017 07:04
-
-
Save qgp9/5ba4cddc28606cdf6f3d4b8e1bee1776 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import basic from './basic.js' | |
import arcodian from './arcodian.js' | |
import modal from './modal.js' | |
const templates = { | |
basic, | |
arcodian, | |
modal | |
} | |
// eslint-disable-next-line no-unused-vars | |
function debug (...args) { args.forEach(v => console.log(v)) } | |
// eslint-disable-next-line no-unused-vars | |
function warn (...args) { args.forEach(v => console.warn(v)) } | |
// eslint-disable-next-line no-unused-vars | |
function error (...args) { args.forEach(v => console.error(v)) } | |
// ** Possible event linst for Animator.next | |
// ** false is same as true. key names are only used. | |
const eventTypes = { | |
next: true, | |
fire: true, | |
animationEnd: true | |
} | |
/** | |
* Help to manage animations. Specially for semantic-ui | |
* @param {string|function} [template=basic] template generator or name | |
*/ | |
class Animator { | |
constructor (template = 'basic') { | |
if (!template) template = templates.basic() | |
else if (typeof template === 'string') { | |
template = templates[template] ? templates[template] : templates['basic'] | |
} | |
if (typeof template === 'function') template = template() | |
this.config = template | |
this.status = this.config.status | |
this.states = this.config.states | |
this.tasks = this.config.tasks | |
this.updateCallback = this.config.updateCallback | |
this.startCallback = this.config.startCallback // NOT implemented yet | |
this.stopCallback = this.config.stopCallback | |
this.nextCounter = 0 | |
this.nextLimit = this.config.nextLimit || 100 | |
} | |
/** | |
* Get task object by taskname | |
* @private | |
* @param {string} taskname Name of task | |
* @return {object} Task | |
*/ | |
getTask (taskname) { | |
return this.tasks[taskname] | |
} | |
/** | |
* Set status to config, and trigger updateCallback | |
* @param {string} taskname | |
* @param {number} step | |
* @param {1|-1} direction | |
* @param {boolean} doCallback | |
*/ | |
setStatus (taskname, step, direction, doCallback = true) { | |
let state = this.tasks[taskname].flow[step] | |
if (typeof state === 'function') state = state('set', this) | |
// const params = state.split('/') | |
// state = params[0] | |
Object.assign(this.status, {taskname, step, state, direction}) | |
debug(this.updateCallback) | |
if (doCallback && this.updateCallback) { | |
debug('=====================================UPDATE') | |
this.updateCallback(this) | |
} | |
} | |
// ************************************************************ | |
// | |
// == Fire | |
// | |
// 1 When given has 'flows' | |
// - Find flow with current state | |
// - Replace a flow with it | |
// - step = 0, direction = 1 | |
// - Start | |
// TODO: Is 3 necessary? | |
// 2. When given task is null or current task | |
// - direction = current if null | |
// - step = current step + above direction if null | |
// - Start | |
// 2. When given task !== current task | |
// - step = 0 if null | |
// - direction = 1 if null | |
// - Start | |
// | |
// ************************************************************ | |
/** | |
* Fire animation event | |
* @param {string} [taskname=null] | |
* @param {number} [step=null] | |
* @direction {1|-1} [direction=null] | |
*/ | |
fire (taskname = null, step = null, direction = null) { | |
debug('fired!!', {taskname, step, direction}) | |
this.nextCounter = 0 | |
let status = this.status | |
taskname = taskname || status.taskname | |
let task = this.getTask(taskname) | |
if (!task) return | |
debug('-- step1', {taskname, step, direction}) | |
// @NOTE flows can task flow array or task name | |
if (task && task.flows) { | |
let flow = task.flows[status.state] || task.flows['default'] | |
if (typeof flow === 'string' && this.tasks[flow]) { | |
flow = this.tasks[flow].flow | |
} | |
if (!flow) return | |
task.flow = flow | |
step = direction = 0 | |
return this.next({taskname, step, direction}, 'fire') | |
} | |
debug('-- step2', {taskname, step, direction}) | |
if (taskname === status.taskname) { | |
if (direction === null) direction = status.direction | |
if (step === null) step = status.step + direction | |
} | |
debug('-- step2', {taskname, step, direction}) | |
if (taskname !== status.taskname) { | |
if (step === null) step = 0 | |
if (direction === null) direction = 1 | |
} | |
return this.next({taskname, step, direction}, 'fire') | |
} | |
/** | |
* proceed next step | |
* @param {object} nstatus Staus of next step | |
* @param {string} nstatus.taskname | |
* @param {number} nstatus.step | |
* @param {-1|0|1} nstatus.direction | |
* @param {string} event Event type. One of 'next', 'fire', 'andmationEnd' | |
*/ | |
next (nstatus = {}, event = 'next') { | |
if (this.nextCounter > this.nextLimit) return | |
this.nextCounter += 1 | |
debug('======== start next ' + this.nextCounter, {event, nstatus}) | |
// == Check parameters | |
if (!(event in eventTypes)) { | |
console.warn(`${event} is not in event types`) | |
return | |
} | |
if (typeof nstatus !== 'object') { | |
console.warn(`nstatus should be an object`) | |
return | |
} | |
// == Current task info | |
const cstatus = this.status | |
const ctaskname = cstatus.taskname | |
const cstep = cstatus.step | |
const cdirection = cstatus.direction | |
const ctask = this.tasks[ctaskname] | |
// Find next/new task/step | |
let taskname | |
let step | |
let state | |
let direction | |
let task | |
debug(this.status) | |
if (nstatus.taskname) { | |
debug('task is given') | |
// When new task name is given | |
taskname = nstatus.taskname | |
step = nstatus.step // TODO error check | |
debug({step}) | |
direction = nstatus.direction | |
task = this.tasks[taskname] | |
if (!task) return | |
if (!direction) direction = cdirection // if not defined, use current | |
} else { | |
// Or next step | |
taskname = ctaskname | |
step = cstep + cdirection | |
direction = cdirection | |
task = ctask | |
} | |
debug('--- configuration done', {taskname, step, direction}) | |
// == for both | |
if (step < 0) { | |
warn(`step ${step} in task ${taskname} is smaller than 0`) | |
return | |
} | |
if (step >= task.flow.length) { | |
// step = task.flow.length - 1 | |
warn(`step ${step} in task ${taskname} is too larger`) | |
return | |
} | |
state = task.flow[step] | |
debug('-- Check state is function', typeof state) | |
if (typeof state === 'function') { | |
const res = state(event, this, {taskname, step, direction, state}) | |
if (res.taskname) taskname = res.taskname | |
if (res.step !== undefined) step = res.step | |
if (res.direction !== undefined) direction = res.direction | |
if (res.action === 'stopNext') { | |
debug('-- Stop Next') | |
return this.setStatus(taskname, step, direction) | |
} | |
if (res.action === 'stop') { | |
debug('-- Stop') | |
return | |
} | |
return this.next({taskname, step, direction}, 'next') | |
} | |
debug(['check delay'], {taskname, step, direction, state}) | |
// == if state is number, delay in ms | |
if (typeof state === 'number' || state.match(/^\d+$/)) { | |
// FIXME: No way to cancel yet | |
return setTimeout(() => this.next({taskname, step: step + direction, direction}), parseInt(state)) | |
} | |
// Stoper - ':' | |
// bouncer - '|' | |
// Finder - ~ | |
// quick pass = * | |
// Diode - '<' '>' | |
// <: :> | |
// <: |> | |
// :> |> | |
// ~> <~ | |
// | |
// | |
// | |
// *> < | |
debug('--- Handle command', {taskname, step, direction, state}, task, this.status) | |
// == Manage special command. except a quick pass '<<', '>>' | |
if (state.match(/^([:<>~*|]+|[<>\d-]+)$/)) { | |
let command = state | |
if (direction < 0) { | |
command = state.split('').reverse().join('') | |
} | |
// PASS | |
if (command[0] === '<' && direction > 0) { | |
const rest = command.slice(1) | |
let jump = 1 | |
if (rest.match(/^\d+$/)) jump = parseInt(rest) | |
step += direction * jump | |
return this.next({taskname, step, direction}, 'next') | |
} | |
if (command[0] === '>' && direction < 0) { | |
const rest = command.slice(1) | |
let jump = 1 | |
if (rest.match(/^\d+$/)) jump = parseInt(rest.split('').reverse().join('')) | |
step += direction * jump | |
return this.next({taskname, step, direction}, 'next') | |
} | |
// STOP | |
if (command[0] === ':') { | |
this.status.taskname = taskname | |
this.status.step = step | |
// keep state | |
if (this.stopCallback) this.stopCallback(this) | |
return | |
} | |
// BOUNCE | |
if (command[0] === '|') { | |
direction = -direction | |
step += 2 * direction | |
if ((task.bounceLimit !== undefined) && task.bounceLimit <= task.bounceCount) return | |
task.bounceLimit += 1 | |
return this.next({taskname, step, direction}, 'next') | |
} | |
// QUICK PASS | |
if (command[0] === '*') { | |
step += direction | |
return this.next({taskname, step, direction}, 'next') | |
} | |
if (command[0].match(/\d/)) { | |
step += direction | |
return this.next({taskname, step, direction}, 'next') | |
} | |
// no matching | |
console.warn(`${state} may not be proper command`) | |
step += direction | |
return this.next({taskname, step, direction}, 'next') | |
} | |
debug('Check status', {taskname, state, step, direction}, {task}) | |
// Matched state !!, Trigger it and return | |
if (task.flow[step]) { | |
this.setStatus(taskname, step, direction, true) | |
// Quick Pass to next | |
const nstate = task.flow[step + direction] | |
debug('check quick pass') | |
if ( | |
nstate && ( | |
(direction < 0 && nstate[1] === '*') || | |
(direction > 0 && nstate[0] === '*') | |
) | |
) { | |
step += direction * 2 | |
debug(['Pass It!!', step]) | |
return this.next({taskname, step, direction}, 'next') | |
} | |
return | |
} | |
debug('Nothing matched', [taskname, step, direction]) | |
// If no match then, just next | |
step += direction | |
return this.next({taskname, step, direction}, 'next') | |
} | |
/** | |
* css class getter | |
* @return {object} css clases of current state | |
*/ | |
class () { | |
let state = this.states[this.status.state] | |
if (typeof state === 'function') { | |
state = state('class', this) | |
} | |
return state | |
} | |
/** | |
* animateEnd event handler | |
* @praam {functon} cb call back function | |
*/ | |
onAnimationEnd (cb) { | |
if (typeof cb === 'function') cb(this) | |
this.next({}, 'animationEnd') | |
} | |
// Alias | |
animationEnd (cb) { this.onAnimationEnd(cb) } | |
andmationend (cb) { this.onAnimationEnd(cb) } | |
} | |
export default Animator |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default function modal () { | |
const config = { | |
status: { | |
taskname: 'init', | |
step: 0, | |
state: 'init', | |
direction: +1 | |
}, | |
updateCallback: null, | |
stopCallback: null, | |
nextLimit: 100, | |
states: { | |
init: { | |
in: false, out: false, animating: false | |
}, | |
in: { | |
in: true, animating: true, visible: true, active: true | |
}, | |
inAfter: { | |
visible: true, active: true | |
}, | |
beforeOut: { | |
out: true, animating: true, visible: true | |
}, | |
out: { | |
out: true, animating: false, hidden: true | |
} | |
}, | |
tasks: { | |
in: { | |
flow: 'in inAfter'.split(' ') | |
}, | |
out: { | |
flow: ['beforeOut', 'out', '*', 100, ':'] | |
}, | |
toggle: { | |
flows: { | |
in: 'out', | |
out: 'in', | |
init: 'in' | |
} | |
} | |
} | |
} | |
return config | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template lang="pug"> | |
div.ui.dimmer.modals.fade.page( | |
:class="merge({}, propClassModals, aniModals.class(updated), {transition})" | |
@click.self="stop" | |
@animationend="aniModals.onAnimationEnd()" | |
) | |
div( | |
style="display:table-cell;text-align:center;vertical-align:middle;" | |
@click.self="stop" | |
) | |
div.ui.modal( | |
:class="merge({}, propClassModal, aniModal.class(updated), {transition})" | |
style="display:inline-block !important;margin:0;position:relative;left:0px !important;" | |
@animationend="aniModal.onAnimationEnd()" | |
) | |
slot | |
div.header Select a Photo | |
</template> | |
<script> | |
import {EasyProps, PropClass} from './mixins' | |
import Animator from './helper/animator' | |
import {merge} from './utils' | |
import {escKeydownEscapeHandler} from './helper/globalSingleKeyHandler' | |
export default { | |
name: 'SemanticModal', | |
mixins: [ | |
EasyProps(), | |
PropClass('modals', 'inverted'), | |
PropClass('modal', | |
'mini', 'tiny', 'small', 'medium', 'big', 'large', 'basic', | |
'vertical', 'horizontal', 'flip', 'fly', 'left', 'right', 'up', 'down', 'fade', 'tada' | |
) | |
], | |
props: { | |
active: [Boolean, false, 'sync'], | |
transition: [Boolean, true], | |
cancelTimeOut: [Number, 500] | |
}, | |
data () { | |
return { | |
updated: 0 | |
} | |
}, | |
beforeMount () { | |
this.aniModal = new Animator('modal') | |
this.aniModal.updateCallback = () => (this.updated = this.updated + 1) | |
this.aniModal.stopCallback = this.stoped | |
this.aniModals = new Animator('modal') | |
this.aniModals.updateCallback = () => (this.updated = this.updated + 1) | |
this.aniModals.stopCallback = this.stoped | |
}, | |
mounted () { | |
this.start() | |
}, | |
watch: { | |
active (val) { | |
if (val) this.start() | |
} | |
}, | |
methods: { | |
merge, | |
start () { | |
if (!this.active) return | |
this.keyEscapeHandler() | |
this.aniModal.fire('in') | |
this.aniModals.fire('in') | |
}, | |
stop () { | |
if (!this.active) return | |
this.aniModal.fire('out') | |
this.aniModals.fire('out') | |
setTimeout(this.stoped, this.cancelTimeOut) | |
}, | |
stoped (context) { | |
if (this.active) this.setActive(false) | |
}, | |
keyEscapeHandler () { | |
// FIXME How to clear listeners if modal is closed with other triggers? | |
escKeydownEscapeHandler(this.stop) | |
} | |
} | |
} | |
</script> | |
<style scope> | |
.ui.modals.visible { | |
position:absolute; | |
display:table !important; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment