Skip to content

Commit

Permalink
Merge branch 'feature/template'
Browse files Browse the repository at this point in the history
  • Loading branch information
onnlucky committed Jul 27, 2017
2 parents f75a057 + 5d14d4d commit 6bc3024
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 22 deletions.
30 changes: 29 additions & 1 deletion framer/Animation.coffee
Expand Up @@ -289,6 +289,8 @@ class exports.Animation extends BaseClass
@_valueUpdaters[k] = @_updateNumericObjectValue.bind(this, ["top", "left", "bottom", "right"])
else if k is "borderRadius"
@_valueUpdaters[k] = @_updateNumericObjectValue.bind(this, ["topLeft", "topRight", "bottomRight", "bottomLeft"])
else if k is "template"
@_valueUpdaters[k] = @_updateTemplateValue
else
@_valueUpdaters[k] = @_updateNumberValue

Expand Down Expand Up @@ -338,6 +340,32 @@ class exports.Animation extends BaseClass
@options.colorModel
)

# shallow mix all end state `{key: value}`s if `value` is a number, otherwise just takes `value`
_updateTemplateValue: (key, value) =>
fromData = @_stateA[key]
toData = @_stateB[key]
targetData = {}

if not _.isObject(toData)
k = @_target._styledText?.buildTemplate()
return if not k
valueB = toData
if _.isNumber(valueB)
valueA = if _.isObject(fromData) then fromData[k] else fromData
valueA = 0 unless _.isNumber(valueA)
valueB = Utils.mapRange(value, 0, 1, valueA, valueB)
targetData[k] = valueB
@_target.template = targetData
return

for k, valueB of toData
if _.isNumber(valueB)
valueA = if _.isObject(fromData) then fromData[k] else fromData
valueA = 0 unless _.isNumber(valueA)
valueB = Utils.mapRange(value, 0, 1, valueA, valueB)
targetData[k] = valueB
@_target.template = targetData

_currentState: ->
return _.pick(@layer, _.keys(@properties))

Expand All @@ -346,7 +374,7 @@ class exports.Animation extends BaseClass

# Special cases that animate with different types of objects
@isAnimatableKey = (k) ->
k in ["gradient", "borderWidth", "borderRadius"]
k in ["gradient", "borderWidth", "borderRadius", "template"]

@filterAnimatableProperties = (properties) ->
# Function to filter only animatable properties out of a given set
Expand Down
85 changes: 83 additions & 2 deletions framer/StyledText.coffee
Expand Up @@ -10,7 +10,7 @@ getMeasureElement = (constraints={}) ->
_measureElement.style.visibility = "hidden"
_measureElement.style.top = "-10000px"
_measureElement.style.left = "-10000px"

# This is a trick to call this function before the document ready event
if not window.document.body
document.write(_measureElement.outerHTML)
Expand Down Expand Up @@ -50,6 +50,12 @@ class InlineStyle
@css = configuration.css
@text = text.substring(@startIndex, @endIndex)

copy: ->
c = new InlineStyle(@text, @css)
c.startIndex = @startIndex
c.endIndex = @endIndex
return c

getOptions: ->
startIndex: @startIndex
endIndex: @endIndex
Expand Down Expand Up @@ -100,6 +106,21 @@ class InlineStyle
@text = @text.replace(regex, replace)
@endIndex = @startIndex + @text.length

addRangesFrom: (regex, block, inline, templateRanges) ->
text = @text
regex.lastIndex = 0
while true
m = regex.exec(text)
return unless m
name = m[1]
return unless name
continue if templateRanges[name]
templateRanges[name] = {block, inline, start: m.index, length: m[0].length, name}

replaceRange: (start, length, text) ->
@text = @text.slice(0, start) + text + @text.slice(start + length)
@endIndex = @startIndex + @text.length

validate: ->
return @startIndex isnt @endIndex and @endIndex is (@startIndex + @text.length)

Expand All @@ -119,6 +140,11 @@ class StyledTextBlock
else
throw new Error("Should specify inlineStyles or css")

copy: ->
c = new StyledTextBlock({text: @text, inlineStyles: []})
c.inlineStyles = @inlineStyles.map((inline) -> inline.copy())
return c

getOptions: ->
text: @text
inlineStyles: @inlineStyles.map((i) -> i.getOptions())
Expand Down Expand Up @@ -204,6 +230,19 @@ class StyledTextBlock
@text = newText
return newText isnt @text

addRangesFrom: (regex, block, templateRanges) ->
@inlineStyles.forEach((inline, index) -> inline.addRangesFrom(regex, block, index, templateRanges))

replaceRange: (inline, start, length, text) ->
currentIndex = 0
for style, index in @inlineStyles
style.startIndex = currentIndex
style.replaceRange(start, length, text) if index is inline
currentIndex += style.text.length
style.endIndex = currentIndex
newText = @inlineStyles.map((i) -> i.text).join('')
@text = newText

validate: ->
combinedText = ''
currentIndex = 0
Expand Down Expand Up @@ -354,9 +393,51 @@ class exports.StyledText
result.height = Math.ceil(measuredHeight)
return result

replace: (search, replace) ->
textReplace: (search, replace) ->
@blocks.map( (b) -> b.replaceText(search, replace))

# must be called first, calling it repeatedly does nothing, returns the first name from the templates
buildTemplate: ->
return @_firstTemplateName if @_templateRanges

# find all "{name}"" text ranges, building a name->{blocks.index,inlines.index,start,length,start} index
regex = /\{\s*(\w+)\s*\}/g
templateRanges = {}
@blocks.forEach((b, index) -> b.addRangesFrom(regex, index, templateRanges))

# turn that into a reverse sorted list of ranges
@_templateRanges = Object.keys(templateRanges).map((k) -> templateRanges[k]).sort((l, r) ->
b = r.block - l.block
return b unless b is 0
i = r.inline - l.inline
return i unless i is 0
r.start - l.start
)
firstRange = @_templateRanges[@_templateRanges.length - 1]
@_firstTemplateName = if firstRange then firstRange.name else null

# we store the initial template data, so template() can be called more than once
@_templateBlocks = @blocks.map((b) -> b.copy())
return @_firstTemplateName

template: (data) ->
# restore the original template
@blocks = @_templateBlocks.map((b) -> b.copy())

# replace all ranges that are in data; @_templateRanges is reverse sorted, so ranges stay valid throughout
for range in @_templateRanges
text = data[range.name]
continue unless text?
text = range.formatter.call(@, text) if _.isFunction(range.formatter)
block = @blocks[range.block]
block.replaceRange(range.inline, range.start, range.length, text)

templateFormatter: (data) ->
for range in @_templateRanges
formatter = data[range.name]
continue unless formatter?
range.formatter = formatter

validate: ->
for block in @blocks
return false if not block.validate()
Expand Down
39 changes: 35 additions & 4 deletions framer/TextLayer.coffee
Expand Up @@ -73,7 +73,10 @@ class exports.TextLayer extends Layer
padding: 0
if not options.font? and not options.fontFamily?
options.fontFamily = @defaultFont()
@_styledText.addBlock options.text, fontSize: "#{options.fontSize}px"

text = options.text
text = String(text) if not _.isString(text)
@_styledText.addBlock text, fontSize: "#{options.fontSize}px"

super options
@__constructor = true
Expand Down Expand Up @@ -239,6 +242,7 @@ class exports.TextLayer extends Layer
@define "text",
get: -> @_styledText.getText()
set: (value) ->
value = String(value) if not _.isString(value)
@_styledText.setText(value)
@renderText()
@emit("change:text", value)
Expand All @@ -255,7 +259,6 @@ class exports.TextLayer extends Layer
layer.text = layer.transform(layer.value) + ''
)


renderText: =>
return if @__constructor
@_styledText.render()
Expand Down Expand Up @@ -284,9 +287,37 @@ class exports.TextLayer extends Layer
defaultFont: ->
return Utils.deviceFont(Framer.Device.platform())

replace: (search, replace) ->
textReplace: (search, replace) ->
oldText = @text
@_styledText.replace(search, replace)
@_styledText.textReplace(search, replace)
if @text isnt oldText
@renderText()
@emit("change:text", @text)

# we remember the template data, and merge it with new data
@define "template",
get: -> @_templateData
set: (data) ->
if not @_templateData then @_templateData = {}

firstName = @_styledText.buildTemplate()
if not _.isObject(data)
return unless firstName
@_templateData[firstName] = data
else
_.assign(@_templateData, data)

oldText = @text
@_styledText.template(@_templateData)
if @text isnt oldText
@renderText()
@emit("change:text", @text)

@define "templateFormatter",
get: -> @_templateFormatter
set: (data) ->
firstName = @_styledText.buildTemplate()
if _.isFunction(data) or not _.isObject(data)
return unless firstName
tmp = {}; tmp[firstName] = data; data = tmp
@_styledText.templateFormatter(data)
39 changes: 39 additions & 0 deletions test/tests/LayerAnimationTest.coffee
Expand Up @@ -1013,3 +1013,42 @@ describe "LayerAnimation", ->
done()
layer.animate
borderWidth: 10

describe "template animations", (done) ->
textLayer = new TextLayer({text: "{distance}{unit}"})
textLayer.templateFormatter =
distance: (v) -> (v / 1000).toFixed(2)
textLayer.template =
unit: "KM"
textLayer.animate
template: 8000
textLayer.on Events.AnimationEnd, ->
textLayer.text.should.equal "8.00KM"
done()

describe "template animations", (done) ->
textLayer = new TextLayer({text: "{distance}{unit}"})
textLayer.templateFormatter =
distance: (v) -> (v / 1000).toFixed(2)
textLayer.template =
unit: "KM"
textLayer.template = 0
textLayer.animate
template: 8000
textLayer.on Events.AnimationEnd, ->
textLayer.text.should.equal "8.00KM"
done()

describe "template animations with multiple properties and formatters", (done) ->
textLayer = new TextLayer({text: "{distance}{unit}-{scale}"})
textLayer.templateFormatter =
distance: (v) -> (v / 1000).toFixed(2)
textLayer.template =
unit: "KM"
textLayer.animate
template:
distance: 8000
scale: "HEAVY"
textLayer.on Events.AnimationEnd, ->
textLayer.text.should.equal "8.00KM-HEAVY"
done()

0 comments on commit 6bc3024

Please sign in to comment.