Skip to content

Instantly share code, notes, and snippets.

@qgp9
Created July 17, 2017 07:04
Show Gist options
  • Save qgp9/5ba4cddc28606cdf6f3d4b8e1bee1776 to your computer and use it in GitHub Desktop.
Save qgp9/5ba4cddc28606cdf6f3d4b8e1bee1776 to your computer and use it in GitHub Desktop.
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
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
}
<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