helpers: CoffeeScript to ES6.
This commit is contained in:
parent
3e26060979
commit
9eb939e38f
@ -1,338 +0,0 @@
|
||||
# FIXME: This file name should reflect what's inside!
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$clone = require 'lodash.clone'
|
||||
$forEach = require 'lodash.foreach'
|
||||
$isArray = require 'lodash.isarray'
|
||||
$isEmpty = require 'lodash.isempty'
|
||||
$isFunction = require 'lodash.isfunction'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$asArray = (val) -> if $isArray val then val else [val]
|
||||
$asFunction = (val) -> if $isFunction val then val else -> val
|
||||
|
||||
$first = (collection, def) ->
|
||||
if (n = collection.length)?
|
||||
return collection[0] unless n is 0
|
||||
else
|
||||
return value for own _, value of collection
|
||||
|
||||
# Nothing was found, returns the `def` value.
|
||||
def
|
||||
|
||||
$removeValue = (array, value) ->
|
||||
index = array.indexOf value
|
||||
return false if index is -1
|
||||
array.splice index, 1
|
||||
true
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# TODO: currently the watch can be updated multiple times per
|
||||
# “$MappedCollection.set()” which is inefficient: it should be
|
||||
# possible to address that.
|
||||
|
||||
$watch = (collection, {
|
||||
# Key(s) of the “remote” objects watched.
|
||||
#
|
||||
# If it is a function, it is evaluated in the scope of the “current”
|
||||
# object. (TODO)
|
||||
#
|
||||
# Default: undefined
|
||||
keys
|
||||
|
||||
# Alias for `keys`.
|
||||
key
|
||||
|
||||
# Rule(s) of the “remote” objects watched.
|
||||
#
|
||||
# If it is a function, it is evaluated in the scope of the “current”
|
||||
# object. (TODO)
|
||||
#
|
||||
# Note: `key`/`keys` and `rule`/`rules` cannot be used both.
|
||||
#
|
||||
# Default: undefined
|
||||
rules
|
||||
|
||||
# Alias for `rules`.
|
||||
rule
|
||||
|
||||
# Value to add to the set.
|
||||
#
|
||||
# If it is a function, it is evaluated in the scope of the “remote”
|
||||
# object.
|
||||
#
|
||||
# Default: -> @val
|
||||
val
|
||||
|
||||
# Predicates the “remote” object must fulfill to be used.
|
||||
#
|
||||
# Default: -> true
|
||||
if: cond
|
||||
|
||||
# Function evaluated in the scope of the “remote” object which
|
||||
# returns the key of the object to update (usually the current one).
|
||||
#
|
||||
# TODO: Does it make sense to return an array?
|
||||
#
|
||||
# Default: undefined
|
||||
bind
|
||||
|
||||
# Initial value.
|
||||
init
|
||||
|
||||
# Function called when a loop is detected.
|
||||
#
|
||||
# Usually it is used to either throw an exception or do nothing to
|
||||
# stop the loop.
|
||||
#
|
||||
# Note: The function may also returns `true` to force the processing
|
||||
# to continue.
|
||||
#
|
||||
# Default: (number_of_loops) -> throw new Error 'loop detected'
|
||||
loopDetected
|
||||
}, fn) ->
|
||||
val = if val is undefined
|
||||
# The default value is simply the value of the item.
|
||||
-> @val
|
||||
else
|
||||
$asFunction val
|
||||
|
||||
loopDetected ?= -> throw new Error 'loop detected'
|
||||
|
||||
# Method allowing the cleanup when the helper is no longer used.
|
||||
#cleanUp = -> # TODO: noop for now.
|
||||
|
||||
# Keys of items using the current helper.
|
||||
consumers = Object.create null
|
||||
|
||||
# Current values.
|
||||
values = Object.create null
|
||||
values.common = init
|
||||
|
||||
# The number of nested processing for this watcher is counted to
|
||||
# avoid an infinite loop.
|
||||
loops = 0
|
||||
|
||||
updating = false
|
||||
|
||||
process = (event, items) ->
|
||||
return if updating
|
||||
|
||||
# Values are grouped by namespace.
|
||||
valuesByNamespace = Object.create null
|
||||
|
||||
$forEach items, (item, key) -> # `key` is a local variable.
|
||||
return unless not cond? or cond.call item
|
||||
|
||||
if bind?
|
||||
key = bind.call item
|
||||
|
||||
# If bind did not return a key, ignores this value.
|
||||
return unless key?
|
||||
|
||||
namespace = "$#{key}"
|
||||
else
|
||||
namespace = 'common'
|
||||
|
||||
# Computes the current value.
|
||||
value = val.call item
|
||||
|
||||
(valuesByNamespace[namespace] ?= []).push value
|
||||
|
||||
return
|
||||
|
||||
# Stops here if no values were computed.
|
||||
return if $isEmpty valuesByNamespace
|
||||
|
||||
if loops
|
||||
return unless (loopDetected loops) is true
|
||||
previousLoops = loops++
|
||||
|
||||
# For each namespace.
|
||||
for namespace, values_ of valuesByNamespace
|
||||
|
||||
# Updates the value.
|
||||
value = values[namespace]
|
||||
ctx = {
|
||||
# TODO: test the $clone
|
||||
value: if value is undefined then $clone init else value
|
||||
}
|
||||
changed = if event is 'enter'
|
||||
fn.call ctx, values_, {}
|
||||
else
|
||||
fn.call ctx, {}, values_
|
||||
|
||||
# Notifies watchers unless it is known the value has not
|
||||
# changed.
|
||||
unless changed is false
|
||||
values[namespace] = ctx.value
|
||||
updating = true
|
||||
if namespace is 'common'
|
||||
collection.touch consumers
|
||||
else
|
||||
collection.touch (namespace.substr 1)
|
||||
updating = false
|
||||
|
||||
loops = previousLoops
|
||||
|
||||
processOne = (event, item) ->
|
||||
process event, [item]
|
||||
|
||||
# Sets up the watch based on the provided criteria.
|
||||
#
|
||||
# TODO: provides a way to clean this when no longer used.
|
||||
keys = $asArray (keys ? key ? [])
|
||||
rules = $asArray (rules ? rule ? [])
|
||||
if not $isEmpty keys
|
||||
# Matching is done on the keys.
|
||||
|
||||
throw new Error 'cannot use keys and rules' unless $isEmpty rules
|
||||
|
||||
$forEach keys, (key) -> collection.on "key=#{key}", processOne
|
||||
|
||||
# Handles existing items.
|
||||
process 'enter', (collection.getRaw keys, true)
|
||||
else if not $isEmpty rules
|
||||
# Matching is done the rules.
|
||||
|
||||
$forEach rules, (rule) -> collection.on "rule=#{rule}", process
|
||||
|
||||
# TODO: Inefficient, is there another way?
|
||||
rules = do -> # Minor optimization.
|
||||
tmp = Object.create null
|
||||
tmp[rule] = true for rule in rules
|
||||
tmp
|
||||
$forEach collection.getRaw(), (item) ->
|
||||
processOne 'enter', item if item.rule of rules
|
||||
else
|
||||
# No matching done.
|
||||
|
||||
collection.on 'any', process
|
||||
|
||||
# Handles existing items.
|
||||
process 'enter', collection.getRaw()
|
||||
|
||||
# Creates the generator: the function which items will used to
|
||||
# register to this watcher and to get the current value.
|
||||
generator = do (key) -> # Declare a local variable.
|
||||
->
|
||||
{key} = this
|
||||
|
||||
# Register this item has a consumer.
|
||||
consumers[key] = true
|
||||
|
||||
# Returns the value for this item if any or the common value.
|
||||
namespace = "$#{key}"
|
||||
if namespace of values
|
||||
values[namespace]
|
||||
else
|
||||
values.common
|
||||
|
||||
# Creates a helper to unregister an item from this watcher.
|
||||
generator.unregister = do (key) -> # Declare a local variable.
|
||||
->
|
||||
{key} = this
|
||||
delete consumers[key]
|
||||
delete values["$#{key}"]
|
||||
|
||||
# Creates a helper to get the value without using an item.
|
||||
generator.raw = (key) ->
|
||||
values[if key? then "$#{key}" else 'common']
|
||||
|
||||
# Returns the generator.
|
||||
generator
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$map = (options) ->
|
||||
options.init = Object.create null
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
changed = false
|
||||
|
||||
$forEach entered, ([key, value]) =>
|
||||
unless @value[key] is value
|
||||
@value[key] = value
|
||||
changed = true
|
||||
return
|
||||
$forEach exited, ([key, value]) =>
|
||||
if key of @value
|
||||
delete @value[key]
|
||||
changed = true
|
||||
return
|
||||
|
||||
changed
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Creates a set of value from various items.
|
||||
$set = (options) ->
|
||||
# Contrary to other helpers, the default value is the key.
|
||||
options.val ?= -> @key
|
||||
|
||||
options.init = []
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
changed = false
|
||||
|
||||
$forEach entered, (value) =>
|
||||
if (@value.indexOf value) is -1
|
||||
@value.push value
|
||||
changed = true
|
||||
return
|
||||
|
||||
$forEach exited, (value) =>
|
||||
changed = true if $removeValue @value, value
|
||||
return
|
||||
|
||||
changed
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
$sum = (options) ->
|
||||
options.init ?= 0
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
prev = @value
|
||||
|
||||
$forEach entered, (value) => @value += value
|
||||
$forEach exited, (value) => @value -= value
|
||||
|
||||
@value isnt prev
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Uses a value from another item.
|
||||
#
|
||||
# Important note: Behavior is not specified when binding to multiple
|
||||
# items.
|
||||
$val = (options) ->
|
||||
# The default value.
|
||||
def = options.default
|
||||
delete options.default
|
||||
|
||||
options.init ?= def
|
||||
|
||||
# Should the last value be kept instead of returning to the default
|
||||
# value when no items are available!
|
||||
keepLast = !!options.keepLast
|
||||
delete options.keepLast
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
prev = @value
|
||||
|
||||
@value = $first entered, (if keepLast then @value else def)
|
||||
|
||||
@value isnt prev
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = {
|
||||
$map
|
||||
$set
|
||||
$sum
|
||||
$val
|
||||
}
|
367
src/helpers.js
Normal file
367
src/helpers.js
Normal file
@ -0,0 +1,367 @@
|
||||
// FIXME: This file name should reflect what's inside!
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import $clone from 'lodash.clone'
|
||||
import $forEach from 'lodash.foreach'
|
||||
import $isArray from 'lodash.isarray'
|
||||
import $isEmpty from 'lodash.isempty'
|
||||
import $isFunction from 'lodash.isfunction'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const $asArray = (val) => $isArray(val) ? val : [val]
|
||||
const $asFunction = (val) => $isFunction(val) ? val : () => val
|
||||
|
||||
const $first = (collection, defaultValue) => {
|
||||
const {length} = collection
|
||||
if (length == null) {
|
||||
for (let key in collection) {
|
||||
return collection[key]
|
||||
}
|
||||
} else if (length) {
|
||||
return collection[0]
|
||||
}
|
||||
|
||||
// Nothing was found, returns the `def` value.
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
const $removeValue = (array, value) => {
|
||||
const index = array.indexOf(value)
|
||||
if (index === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
array.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: currently the watch can be updated multiple times per
|
||||
// “$MappedCollection.set()” which is inefficient: it should be
|
||||
// possible to address that.
|
||||
|
||||
const $watch = (collection, {
|
||||
// Key(s) of the “remote” objects watched.
|
||||
//
|
||||
// If it is a function, it is evaluated in the scope of the “current”
|
||||
// object. (TODO)
|
||||
//
|
||||
// Default: undefined
|
||||
keys,
|
||||
|
||||
// Alias for `keys`.
|
||||
key,
|
||||
|
||||
// Rule(s) of the “remote” objects watched.
|
||||
//
|
||||
// If it is a function, it is evaluated in the scope of the “current”
|
||||
// object. (TODO)
|
||||
//
|
||||
// Note: `key`/`keys` and `rule`/`rules` cannot be used both.
|
||||
//
|
||||
// Default: undefined
|
||||
rules,
|
||||
|
||||
// Alias for `rules`.
|
||||
rule,
|
||||
|
||||
// Value to add to the set.
|
||||
//
|
||||
// If it is a function, it is evaluated in the scope of the “remote”
|
||||
// object.
|
||||
//
|
||||
// Default: -> @val
|
||||
val,
|
||||
|
||||
// Predicates the “remote” object must fulfill to be used.
|
||||
//
|
||||
// Default: -> true
|
||||
if: cond,
|
||||
|
||||
// Function evaluated in the scope of the “remote” object which
|
||||
// returns the key of the object to update (usually the current one).
|
||||
//
|
||||
// TODO: Does it make sense to return an array?
|
||||
//
|
||||
// Default: undefined
|
||||
bind,
|
||||
|
||||
// Initial value.
|
||||
init,
|
||||
|
||||
// Function called when a loop is detected.
|
||||
//
|
||||
// Usually it is used to either throw an exception or do nothing to
|
||||
// stop the loop.
|
||||
//
|
||||
// Note: The function may also returns `true` to force the processing
|
||||
// to continue.
|
||||
loopDetected = () => { throw new Error('loop detected') }
|
||||
}, fn) => {
|
||||
val = val == null ?
|
||||
// The default value is simply the value of the item.
|
||||
function () { return this.val } :
|
||||
$asFunction(val)
|
||||
|
||||
// Method allowing the cleanup when the helper is no longer used.
|
||||
// cleanUp = -> // TODO: noop for now.
|
||||
|
||||
// Keys of items using the current helper.
|
||||
const consumers = Object.create(null)
|
||||
|
||||
// Current values.
|
||||
const values = Object.create(null)
|
||||
values.common = init
|
||||
|
||||
// The number of nested processing for this watcher is counted to
|
||||
// avoid an infinite loop.
|
||||
let loops = 0
|
||||
|
||||
let updating = false
|
||||
|
||||
const process = (event, items) => {
|
||||
if (updating) return
|
||||
|
||||
// Values are grouped by namespace.
|
||||
const valuesByNamespace = Object.create(null)
|
||||
|
||||
$forEach(items, (item) => {
|
||||
if (cond && !cond.call(item)) return
|
||||
|
||||
const namespace = (function () {
|
||||
if (bind) {
|
||||
const key = bind.call(item)
|
||||
|
||||
return key && `$${key}`
|
||||
} else {
|
||||
return 'common'
|
||||
}
|
||||
})()
|
||||
|
||||
// If not namespace, ignore this item.
|
||||
if (!namespace) return
|
||||
|
||||
(
|
||||
valuesByNamespace[namespace] ||
|
||||
(valuesByNamespace[namespace] = [])
|
||||
).push(val.call(item))
|
||||
})
|
||||
|
||||
// Stops here if no values were computed.
|
||||
if ($isEmpty(valuesByNamespace)) return
|
||||
|
||||
if (loops && loopDetected(loops) !== true) return
|
||||
const previousLoops = loops++
|
||||
|
||||
// For each namespace.
|
||||
$forEach(valuesByNamespace, (values_, namespace) => {
|
||||
// Updates the value.
|
||||
const value = values[namespace]
|
||||
|
||||
const ctx = {
|
||||
// TODO: test the $clone
|
||||
value: value == null ? $clone(init) : value
|
||||
}
|
||||
const changed = event === 'enter' ?
|
||||
fn.call(ctx, values_, {}) :
|
||||
fn.call(ctx, {}, values_)
|
||||
|
||||
// Notifies watchers unless it is known the value has not
|
||||
// changed.
|
||||
if (changed !== false) {
|
||||
values[namespace] = ctx.value
|
||||
updating = true
|
||||
if (namespace === 'common') {
|
||||
collection.touch(consumers)
|
||||
} else {
|
||||
collection.touch(namespace.substr(1))
|
||||
}
|
||||
updating = false
|
||||
}
|
||||
})
|
||||
|
||||
loops = previousLoops
|
||||
}
|
||||
|
||||
const processOne = (event, item) => process(event, [item])
|
||||
|
||||
// Sets up the watch based on the provided criteria.
|
||||
//
|
||||
// TODO: provides a way to clean this when no longer used.
|
||||
keys = $asArray(keys || key || [])
|
||||
rules = $asArray(rules || rule || [])
|
||||
if (!$isEmpty(keys)) {
|
||||
// Matching is done on the keys.
|
||||
|
||||
if (!$isEmpty(rules)) {
|
||||
throw new Error('cannot use both keys and rules')
|
||||
}
|
||||
|
||||
$forEach(keys, key => {
|
||||
collection.on(`key=${key}`, processOne)
|
||||
})
|
||||
|
||||
// Handles existing items.
|
||||
process('enter', collection.getRaw(keys, true))
|
||||
} else if (!$isEmpty(rules)) {
|
||||
// Matching is done the rules.
|
||||
|
||||
$forEach(rules, rule => {
|
||||
collection.on(`rule=${rule}`, process)
|
||||
})
|
||||
|
||||
// TODO: Inefficient, is there another way?
|
||||
rules = (function (rules) { // Minor optimization.
|
||||
const tmp = Object.create(null)
|
||||
for (let rule of rules) {
|
||||
tmp[rule] = true
|
||||
}
|
||||
return tmp
|
||||
})(rules)
|
||||
$forEach(collection.getRaw(), item => {
|
||||
if (rules[item.rule]) {
|
||||
processOne('enter', item)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// No matching done.
|
||||
|
||||
collection.on('any', process)
|
||||
|
||||
// Handles existing items.
|
||||
process('enter', collection.getRaw())
|
||||
}
|
||||
|
||||
// Creates the generator: the function which items will used to
|
||||
// register to this watcher and to get the current value.
|
||||
const generator = function () {
|
||||
const {key} = this
|
||||
|
||||
// Register this item has a consumer.
|
||||
consumers[key] = true
|
||||
|
||||
// Returns the value for this item if any or the common value.
|
||||
const namespace = `$${key}`
|
||||
return (namespace in values) ?
|
||||
values[namespace] :
|
||||
values.common
|
||||
}
|
||||
|
||||
// Creates a helper to unregister an item from this watcher.
|
||||
generator.unregister = function () {
|
||||
const {key} = this
|
||||
delete consumers[key]
|
||||
delete values[`$${key}`]
|
||||
}
|
||||
|
||||
// Creates a helper to get the value without using an item.
|
||||
generator.raw = (key) => values[key != null ? `$${key}` : 'common']
|
||||
|
||||
// Returns the generator.
|
||||
return generator
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const $map = function (options) {
|
||||
options.init = Object.create(null)
|
||||
|
||||
return $watch(this, options, function (entered, exited) {
|
||||
let changed = false
|
||||
|
||||
$forEach(entered, ([key, value]) => {
|
||||
if (this.value[key] !== value) {
|
||||
this.value[key] = value
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
$forEach(exited, ([key, value]) => {
|
||||
if (key in this.value) {
|
||||
delete this.value[key]
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
|
||||
return changed
|
||||
})
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Creates a set of value from various items.
|
||||
export const $set = function (options) {
|
||||
// Contrary to other helpers, the default value is the key.
|
||||
if (!options.val) {
|
||||
options.val = function () { return this.key }
|
||||
}
|
||||
|
||||
options.init = []
|
||||
|
||||
return $watch(this, options, function (entered, exited) {
|
||||
let changed = false
|
||||
|
||||
$forEach(entered, (value) => {
|
||||
if (this.value.indexOf(value) === -1) {
|
||||
this.value.push(value)
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
|
||||
$forEach(exited, (value) => {
|
||||
if ($removeValue(this.value, value)) {
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
|
||||
return changed
|
||||
})
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const $sum = function (options) {
|
||||
if (!options.init) {
|
||||
options.init = 0
|
||||
}
|
||||
|
||||
return $watch(this, options, function (entered, exited) {
|
||||
const prev = this.value
|
||||
|
||||
$forEach(entered, (value) => { this.value += value })
|
||||
$forEach(exited, (value) => { this.value -= value })
|
||||
|
||||
return this.value !== prev
|
||||
})
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Uses a value from another item.
|
||||
//
|
||||
// Important note: Behavior is not specified when binding to multiple
|
||||
// items.
|
||||
export const $val = function (options) {
|
||||
// The default value.
|
||||
const def = options.default
|
||||
delete options.default
|
||||
|
||||
if (!options.init) {
|
||||
options.init = def
|
||||
}
|
||||
|
||||
// Should the last value be kept instead of returning to the default
|
||||
// value when no items are available!
|
||||
const keepLast = !!options.keepLast
|
||||
delete options.keepLast
|
||||
|
||||
return $watch(this, options, function (entered, exited) {
|
||||
const prev = this.value
|
||||
|
||||
this.value = $first(entered, keepLast ? this.value : def)
|
||||
|
||||
return this.value !== prev
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user