From 808654646862491d14b3c735c5b2033f528836f5 Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Tue, 25 Jul 2017 17:38:51 +0200 Subject: [PATCH 01/10] add TextLayer.template --- framer/StyledText.coffee | 69 ++++++++++++++++++++++++++++++++- framer/TextLayer.coffee | 6 +++ test/tests/TextLayerTest.coffee | 33 ++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/framer/StyledText.coffee b/framer/StyledText.coffee index 87af33033..2d5c2794c 100644 --- a/framer/StyledText.coffee +++ b/framer/StyledText.coffee @@ -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) @@ -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 @@ -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) @@ -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()) @@ -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 @@ -357,6 +396,34 @@ class exports.StyledText replace: (search, replace) -> @blocks.map( (b) -> b.replaceText(search, replace)) + template: (data) -> + # we store the initial template data, so template() can be called more than once + if not @_templateRanges + # find all "{name}"" text ranges, building a name->{blocks.index,inlines.index,start,length,start} index + regex = new RegExp("\\{\\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 + ) + @_templateBlocks = @blocks.map((b) -> b.copy()) + else + # 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 + block = @blocks[range.block] + block.replaceRange(range.inline, range.start, range.length, text) + validate: -> for block in @blocks return false if not block.validate() diff --git a/framer/TextLayer.coffee b/framer/TextLayer.coffee index 78709b5ae..bbb97c997 100644 --- a/framer/TextLayer.coffee +++ b/framer/TextLayer.coffee @@ -290,3 +290,9 @@ class exports.TextLayer extends Layer if @text isnt oldText @renderText() @emit("change:text", @text) + + # data = {name: "replacement text", ...} + template: (data) -> + @_styledText.template(data) + @renderText() + @emit("change:text", @text) diff --git a/test/tests/TextLayerTest.coffee b/test/tests/TextLayerTest.coffee index d6d68949d..cdd2dd430 100644 --- a/test/tests/TextLayerTest.coffee +++ b/test/tests/TextLayerTest.coffee @@ -8,6 +8,39 @@ simpleStyledTextOptions = {blocks: [{inlineStyles: [{startIndex: 0, endIndex: 5, exampleStyledTextOptions = {blocks: [{inlineStyles: [{startIndex: 0, endIndex: 6, css: {fontSize: "48px", WebkitTextFillColor: "#000000", letterSpacing: "0px", fontWeight: 800, lineHeight: "1.2", tabSize: 4, fontFamily: "'.SFNSText-Heavy', '.SFUIText-Heavy', 'SF UI Text', 'Times New Roman'"}}], text: "Header"}, {inlineStyles: [{startIndex: 0, endIndex: 8, css: {fontSize: "20px", WebkitTextFillColor: "rgb(153, 153, 153)", letterSpacing: "0px", fontWeight: 400, lineHeight: "1.2", tabSize: 4, fontFamily: "'.SFNSText', 'SFUIText-Regular', '.SFUIText', 'SF UI Text', 'Times New Roman'"}}], text: "Subtitle"}, {inlineStyles: [{startIndex: 0, endIndex: 6, css: {fontSize: "16px", WebkitTextFillColor: "rgb(238, 68, 68)", letterSpacing: "0px", fontWeight: 200, lineHeight: "1.2", tabSize: 4, fontFamily: "'.SFNSText-Light', 'SFUIText-Light', '.SFUIText-Light', 'SF UI Text', 'Times New Roman'"}}, {startIndex: 6, endIndex: 16, css: {fontSize: "12px", WebkitTextFillColor: "#000000", letterSpacing: "0px", fontWeight: 400, lineHeight: "1.2", tabSize: 4, fontFamily: "'.SFNSText', 'SFUIText-Regular', '.SFUIText', 'SF UI Text', 'Times New Roman'"}}], text: "Leader Body text"}], alignment: "left"} differentFonts = {"blocks": [{"inlineStyles": [{"startIndex": 0, "endIndex": 14, "css": {"fontSize": "60px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'.SFNSText', 'SFUIText-Regular', '.SFUIText', 'SF UI Text', sans-serif"}}], "text": "This is Roboto"}, {"inlineStyles": [{"startIndex": 0, "endIndex": 0, "css": {"fontSize": "47px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'Roboto-Regular', 'Roboto', sans-serif"}}], "text": ""}, {"inlineStyles": [{"startIndex": 0, "endIndex": 14, "css": {"fontSize": "60px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'Roboto-Regular', 'Roboto', sans-serif"}}], "text": "This is Roboto"}, {"inlineStyles": [{"startIndex": 0, "endIndex": 27, "css": {"fontSize": "60px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'VesperLibre-Regular', 'Vesper Libre', serif"}}], "text": "With a little bit of Vesper"}, {"inlineStyles": [{"startIndex": 0, "endIndex": 0, "css": {"fontSize": "10px", "WebkitTextFillColor": "rgb(255, 0, 0)", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'Lato-Regular', 'Lato', serif"}}], "text": ""}, {"inlineStyles": [{"startIndex": 0, "endIndex": 16, "css": {"fontSize": "12px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'Alcubierre', serif"}}], "text": "And some Raleway"}]} +describe.only "TextLayer.template", -> + it "should work", -> + text = new TextLayer({text: "xxx {hello} xxx"}) + text.template({hello: "xxx"}) + text.text.should.eql "xxx xxx xxx" + text._styledText.validate().should.equal true + + text.template({hello: "again"}) + text.text.should.eql "xxx again xxx" + + text.replace("again", "HELLO THIS IS ME AND MORE") + text.text = "" + text.text.should.eql "" + + text.template({hello: "still works"}) + text.text.should.eql "xxx still works xxx" + text._styledText.validate().should.equal true + + it "should expand many blocks", -> + text = new TextLayer({text:"{a},{b},{c},{d}"}) + text.template({a: "AAAAA"}) + text.text.should.eql "AAAAA,{b},{c},{d}" + text.template({a: "AAAAA", b: "BEE"}) + text.text.should.eql "AAAAA,BEE,{c},{d}" + text.template({a: "AAAAA", b: "BEE", c: "CEEEE"}) + text.text.should.eql "AAAAA,BEE,CEEEE,{d}" + text.template({a: "AAAAA", b: "BEE", c: "CEEEE", d: "DEE"}) + text.text.should.eql "AAAAA,BEE,CEEEE,DEE" + + it "should support multiple blocks and inline styles", -> + text = new TextLayer({text:"{a}\n{b},{c}\n{d}"}) + text.template({a: "AAAAAAA", b: "BEE", c: "CEEEE", d: "DEE"}) + text.text.should.eql "AAAAAAA\nBEE,CEEEE\nDEE" describe "TextLayer", -> describe "defaults", -> From 1ab1460bb928976fbe4b65987efada8eb93dd2d3 Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Wed, 26 Jul 2017 08:42:24 +0200 Subject: [PATCH 02/10] fix empty string template; multiline also works without new blocks --- framer/StyledText.coffee | 2 +- framer/TextLayer.coffee | 1 - test/tests/TextLayerTest.coffee | 11 +++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/framer/StyledText.coffee b/framer/StyledText.coffee index 2d5c2794c..82528717f 100644 --- a/framer/StyledText.coffee +++ b/framer/StyledText.coffee @@ -420,7 +420,7 @@ class exports.StyledText # 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 + continue unless text? block = @blocks[range.block] block.replaceRange(range.inline, range.start, range.length, text) diff --git a/framer/TextLayer.coffee b/framer/TextLayer.coffee index bbb97c997..5f4cbab8b 100644 --- a/framer/TextLayer.coffee +++ b/framer/TextLayer.coffee @@ -255,7 +255,6 @@ class exports.TextLayer extends Layer layer.text = layer.transform(layer.value) + '' ) - renderText: => return if @__constructor @_styledText.render() diff --git a/test/tests/TextLayerTest.coffee b/test/tests/TextLayerTest.coffee index cdd2dd430..36ee6388c 100644 --- a/test/tests/TextLayerTest.coffee +++ b/test/tests/TextLayerTest.coffee @@ -18,6 +18,10 @@ describe.only "TextLayer.template", -> text.template({hello: "again"}) text.text.should.eql "xxx again xxx" + text.template({hello: ""}) + text.text.should.eql "xxx xxx" + + text.replace("again", "HELLO THIS IS ME AND MORE") text.text = "" text.text.should.eql "" @@ -42,6 +46,13 @@ describe.only "TextLayer.template", -> text.template({a: "AAAAAAA", b: "BEE", c: "CEEEE", d: "DEE"}) text.text.should.eql "AAAAAAA\nBEE,CEEEE\nDEE" + it "should support inserting multilines", -> + text = new TextLayer({text:"{a}\n{b},{c}\n{d}"}) + text._styledText.blocks.length.should.eql 3 + text.template({a: "ALONG", b:"BELOW\nMORE STUFF\nOEPS", c: "CIRCLE", d:"DEAF"}) + text.text.should.eql "ALONG\nBELOW\nMORE STUFF\nOEPS,CIRCLE\nDEAF" + # text._styledText.blocks.length.should.eql 5 + describe "TextLayer", -> describe "defaults", -> it "should set the correct defaults", -> From b88e9fc6f6e34a81a585ed3a9b164b6d023462da Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Wed, 26 Jul 2017 12:28:55 +0200 Subject: [PATCH 03/10] implement template sugar by replacing in order --- framer/StyledText.coffee | 17 ++++++++++++- framer/TextLayer.coffee | 5 +++- test/tests/TextLayerTest.coffee | 44 ++++++++++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/framer/StyledText.coffee b/framer/StyledText.coffee index 82528717f..d65a159d6 100644 --- a/framer/StyledText.coffee +++ b/framer/StyledText.coffee @@ -396,7 +396,7 @@ class exports.StyledText replace: (search, replace) -> @blocks.map( (b) -> b.replaceText(search, replace)) - template: (data) -> + template: (data, list) -> # we store the initial template data, so template() can be called more than once if not @_templateRanges # find all "{name}"" text ranges, building a name->{blocks.index,inlines.index,start,length,start} index @@ -417,6 +417,21 @@ class exports.StyledText # restore the original template @blocks = @_templateBlocks.map((b) -> b.copy()) + # replace a list of arguments in order + if list + if list.length > @_templateRanges.length + list = list.slice(0, @_templateRanges.length) + list.reverse() # template ranges is in reverse order + first = @_templateRanges.length - list.length + for range, index in @_templateRanges + continue if index < first + text = list[index - first] + continue unless text? + block = @blocks[range.block] + block.replaceRange(range.inline, range.start, range.length, text) + return + return unless data + # replace all ranges that are in data; @_templateRanges is reverse sorted, so ranges stay valid throughout for range in @_templateRanges text = data[range.name] diff --git a/framer/TextLayer.coffee b/framer/TextLayer.coffee index 5f4cbab8b..6db0ba8ef 100644 --- a/framer/TextLayer.coffee +++ b/framer/TextLayer.coffee @@ -292,6 +292,9 @@ class exports.TextLayer extends Layer # data = {name: "replacement text", ...} template: (data) -> - @_styledText.template(data) + if not _.isObject(data) + list = _.concat([], arguments) + data = null + @_styledText.template(data, list) @renderText() @emit("change:text", @text) diff --git a/test/tests/TextLayerTest.coffee b/test/tests/TextLayerTest.coffee index 36ee6388c..4f919014e 100644 --- a/test/tests/TextLayerTest.coffee +++ b/test/tests/TextLayerTest.coffee @@ -21,6 +21,8 @@ describe.only "TextLayer.template", -> text.template({hello: ""}) text.text.should.eql "xxx xxx" + text.template({hello: 42}) + text.text.should.eql "xxx 42 xxx" text.replace("again", "HELLO THIS IS ME AND MORE") text.text = "" @@ -31,7 +33,7 @@ describe.only "TextLayer.template", -> text._styledText.validate().should.equal true it "should expand many blocks", -> - text = new TextLayer({text:"{a},{b},{c},{d}"}) + text = new TextLayer({text: "{a},{b},{c},{d}"}) text.template({a: "AAAAA"}) text.text.should.eql "AAAAA,{b},{c},{d}" text.template({a: "AAAAA", b: "BEE"}) @@ -40,19 +42,53 @@ describe.only "TextLayer.template", -> text.text.should.eql "AAAAA,BEE,CEEEE,{d}" text.template({a: "AAAAA", b: "BEE", c: "CEEEE", d: "DEE"}) text.text.should.eql "AAAAA,BEE,CEEEE,DEE" + text._styledText.validate().should.equal true it "should support multiple blocks and inline styles", -> - text = new TextLayer({text:"{a}\n{b},{c}\n{d}"}) + text = new TextLayer({text: "{a}\n{b},{c}\n{d}"}) text.template({a: "AAAAAAA", b: "BEE", c: "CEEEE", d: "DEE"}) text.text.should.eql "AAAAAAA\nBEE,CEEEE\nDEE" + text._styledText.validate().should.equal true + it "should support inserting multilines", -> - text = new TextLayer({text:"{a}\n{b},{c}\n{d}"}) + text = new TextLayer({text: "{a}\n{b},{c}\n{d}"}) text._styledText.blocks.length.should.eql 3 - text.template({a: "ALONG", b:"BELOW\nMORE STUFF\nOEPS", c: "CIRCLE", d:"DEAF"}) + text.template({a: "ALONG", b: "BELOW\nMORE STUFF\nOEPS", c: "CIRCLE", d: "DEAF"}) text.text.should.eql "ALONG\nBELOW\nMORE STUFF\nOEPS,CIRCLE\nDEAF" + text._styledText.validate().should.equal true # text._styledText.blocks.length.should.eql 5 + it "should take a numbers, booleans", -> + text = new TextLayer({text: "{a}"}) + text.template({a: 42}) + text.text.should.eql "42" + + text.template({a: false}) + text.text.should.eql "false" + + it "should not take null or nothing", -> + text = new TextLayer({text: "{a}"}) + text.template({a: null}) + text.text.should.eql "{a}" + + text.template({b: "HELLO"}) + text.text.should.eql "{a}" + + it "should support no names sugar", -> + text = new TextLayer({text: "{a}\n{b},{c}\n{d}"}) + text.template("A", "B", "C", 42) + text.text.should.eql "A\nB,C\n42" + + text.template("A", "B", "C", 42, "and", "too", "many") + text.text.should.eql "A\nB,C\n42" + + text.template(true, 42) + text.text.should.eql "true\n42,{c}\n{d}" + + text.template() + text.text.should.eql "{a}\n{b},{c}\n{d}" + describe "TextLayer", -> describe "defaults", -> it "should set the correct defaults", -> From 11012d43883eb388af1a696027acb56eb79e33fc Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Wed, 26 Jul 2017 13:54:20 +0200 Subject: [PATCH 04/10] TextLayer.replace() -> TextLayer.textReplace() --- framer/StyledText.coffee | 4 ++-- framer/TextLayer.coffee | 4 ++-- test/tests/TextLayerTest.coffee | 26 +++++++++++++------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/framer/StyledText.coffee b/framer/StyledText.coffee index d65a159d6..0cbd5fb27 100644 --- a/framer/StyledText.coffee +++ b/framer/StyledText.coffee @@ -393,14 +393,14 @@ class exports.StyledText result.height = Math.ceil(measuredHeight) return result - replace: (search, replace) -> + textReplace: (search, replace) -> @blocks.map( (b) -> b.replaceText(search, replace)) template: (data, list) -> # we store the initial template data, so template() can be called more than once if not @_templateRanges # find all "{name}"" text ranges, building a name->{blocks.index,inlines.index,start,length,start} index - regex = new RegExp("\\{\\s*(\\w+)\\s*\\}", "g") + regex = /\{\s*(\w+)\s*\}/g templateRanges = {} @blocks.forEach((b, index) -> b.addRangesFrom(regex, index, templateRanges)) diff --git a/framer/TextLayer.coffee b/framer/TextLayer.coffee index 6db0ba8ef..5c22eb282 100644 --- a/framer/TextLayer.coffee +++ b/framer/TextLayer.coffee @@ -283,9 +283,9 @@ 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) diff --git a/test/tests/TextLayerTest.coffee b/test/tests/TextLayerTest.coffee index 4f919014e..5c097f7be 100644 --- a/test/tests/TextLayerTest.coffee +++ b/test/tests/TextLayerTest.coffee @@ -24,7 +24,7 @@ describe.only "TextLayer.template", -> text.template({hello: 42}) text.text.should.eql "xxx 42 xxx" - text.replace("again", "HELLO THIS IS ME AND MORE") + text.textReplace("again", "HELLO THIS IS ME AND MORE") text.text = "" text.text.should.eql "" @@ -601,7 +601,7 @@ describe "TextLayer", -> it "should replace the full text", -> searchText = "Search text" subject.text = searchText - subject.replace(searchText, "Replacement") + subject.textReplace(searchText, "Replacement") subject.text.should.equal "Replacement" subject._styledText.validate().should.equal true @@ -609,7 +609,7 @@ describe "TextLayer", -> searchText = "Search text" replaceText = "Replacement" subject.text = searchText - subject.replace(searchText, replaceText) + subject.textReplace(searchText, replaceText) style = subject._styledText.blocks[0].inlineStyles[0] style.startIndex.should.equal 0 style.endIndex.should.equal replaceText.length @@ -617,52 +617,52 @@ describe "TextLayer", -> subject._styledText.validate().should.equal true it "should replace partial text", -> - subject.replace("ea", "oooo") + subject.textReplace("ea", "oooo") subject.text.should.equal "Hooooder\nSubtitle\nLooooder Body text" - subject.replace("o", "%") + subject.textReplace("o", "%") subject.text.should.equal "H%%%%der\nSubtitle\nL%%%%der B%dy text" subject._styledText.validate().should.equal true it "should handle replacing with the same text correctly", -> - subject.replace("e", "e") + subject.textReplace("e", "e") subject.text.should.equal "Header\nSubtitle\nLeader Body text" - subject.replace("e", "ee") + subject.textReplace("e", "ee") subject.text.should.equal "Heeadeer\nSubtitlee\nLeeadeer Body teext" subject._styledText.validate().should.equal true it "should keep the styling in place when replacing text", -> searchText = "Search text" subject.text = searchText - subject.replace(searchText, "Replacement") + subject.textReplace(searchText, "Replacement") subject._styledText.blocks[0].inlineStyles[0].css.should.eql exampleStyledTextOptions.blocks[0].inlineStyles[0].css subject._styledText.validate().should.equal true it "should apply the style to the replaced partial text", -> - subject.replace("e", "xxx") + subject.textReplace("e", "xxx") for block, blockIndex in subject._styledText.blocks for style, styleIndex in block.inlineStyles style.css.should.eql exampleStyledTextOptions.blocks[blockIndex].inlineStyles[styleIndex].css subject._styledText.validate().should.equal true it "should work with regexes", -> - subject.replace(/d[ey]+/, "die") + subject.textReplace(/d[ey]+/, "die") subject.text.should.equal "Headier\nSubtitle\nLeadier Bodie text" subject._styledText.validate().should.equal true it "should rerender the text when replacing it", -> htmlBefore = subject.html - subject.replace("a", "b") + subject.textReplace("a", "b") subject.html.should.not.equal htmlBefore it "should emit change:text event only when the text has changed", (done) -> subject.on "change:text", -> done() - subject.replace("a", "b") + subject.textReplace("a", "b") it "should not emit a change:text event when the text doesn't change", (done) -> subject.on "change:text", -> throw new Error("change:text event should not be emitted") - subject.replace("e", "e") + subject.textReplace("e", "e") done() describe "value transformer", -> From 7145545dafade98f35cdd93619b716f11e3894ec Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Wed, 26 Jul 2017 14:49:12 +0200 Subject: [PATCH 05/10] allow setting things other than TextLayer.text by String(text) --- framer/TextLayer.coffee | 6 +++++- test/tests/TextLayerTest.coffee | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/framer/TextLayer.coffee b/framer/TextLayer.coffee index 5c22eb282..c5da9dafe 100644 --- a/framer/TextLayer.coffee +++ b/framer/TextLayer.coffee @@ -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 @@ -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) diff --git a/test/tests/TextLayerTest.coffee b/test/tests/TextLayerTest.coffee index 5c097f7be..c531beb03 100644 --- a/test/tests/TextLayerTest.coffee +++ b/test/tests/TextLayerTest.coffee @@ -8,7 +8,7 @@ simpleStyledTextOptions = {blocks: [{inlineStyles: [{startIndex: 0, endIndex: 5, exampleStyledTextOptions = {blocks: [{inlineStyles: [{startIndex: 0, endIndex: 6, css: {fontSize: "48px", WebkitTextFillColor: "#000000", letterSpacing: "0px", fontWeight: 800, lineHeight: "1.2", tabSize: 4, fontFamily: "'.SFNSText-Heavy', '.SFUIText-Heavy', 'SF UI Text', 'Times New Roman'"}}], text: "Header"}, {inlineStyles: [{startIndex: 0, endIndex: 8, css: {fontSize: "20px", WebkitTextFillColor: "rgb(153, 153, 153)", letterSpacing: "0px", fontWeight: 400, lineHeight: "1.2", tabSize: 4, fontFamily: "'.SFNSText', 'SFUIText-Regular', '.SFUIText', 'SF UI Text', 'Times New Roman'"}}], text: "Subtitle"}, {inlineStyles: [{startIndex: 0, endIndex: 6, css: {fontSize: "16px", WebkitTextFillColor: "rgb(238, 68, 68)", letterSpacing: "0px", fontWeight: 200, lineHeight: "1.2", tabSize: 4, fontFamily: "'.SFNSText-Light', 'SFUIText-Light', '.SFUIText-Light', 'SF UI Text', 'Times New Roman'"}}, {startIndex: 6, endIndex: 16, css: {fontSize: "12px", WebkitTextFillColor: "#000000", letterSpacing: "0px", fontWeight: 400, lineHeight: "1.2", tabSize: 4, fontFamily: "'.SFNSText', 'SFUIText-Regular', '.SFUIText', 'SF UI Text', 'Times New Roman'"}}], text: "Leader Body text"}], alignment: "left"} differentFonts = {"blocks": [{"inlineStyles": [{"startIndex": 0, "endIndex": 14, "css": {"fontSize": "60px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'.SFNSText', 'SFUIText-Regular', '.SFUIText', 'SF UI Text', sans-serif"}}], "text": "This is Roboto"}, {"inlineStyles": [{"startIndex": 0, "endIndex": 0, "css": {"fontSize": "47px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'Roboto-Regular', 'Roboto', sans-serif"}}], "text": ""}, {"inlineStyles": [{"startIndex": 0, "endIndex": 14, "css": {"fontSize": "60px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'Roboto-Regular', 'Roboto', sans-serif"}}], "text": "This is Roboto"}, {"inlineStyles": [{"startIndex": 0, "endIndex": 27, "css": {"fontSize": "60px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'VesperLibre-Regular', 'Vesper Libre', serif"}}], "text": "With a little bit of Vesper"}, {"inlineStyles": [{"startIndex": 0, "endIndex": 0, "css": {"fontSize": "10px", "WebkitTextFillColor": "rgb(255, 0, 0)", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'Lato-Regular', 'Lato', serif"}}], "text": ""}, {"inlineStyles": [{"startIndex": 0, "endIndex": 16, "css": {"fontSize": "12px", "WebkitTextFillColor": "#000000", "letterSpacing": "0px", "fontWeight": 400, "lineHeight": "1.2", "tabSize": 4, "fontFamily": "'Alcubierre', serif"}}], "text": "And some Raleway"}]} -describe.only "TextLayer.template", -> +describe "TextLayer.template", -> it "should work", -> text = new TextLayer({text: "xxx {hello} xxx"}) text.template({hello: "xxx"}) @@ -159,6 +159,17 @@ describe "TextLayer", -> styledText: differentFonts l._styledText.blocks.length.should.equal 6 + it "should convert number values", -> + l = new TextLayer({text: 333}) + l.text.should.equal "333" + l.text = 42 + l.text.should.equal "42" + + it "should err on other values", -> + l = new TextLayer + l.text = {} + l.text.should.eql "[object Object]" + describe "animation", -> it "should start animating from the textcolor the layer has", (done) -> text = new TextLayer From 73cb55e254d179fbe52eeabba34007df42ea0c90 Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Wed, 26 Jul 2017 14:49:32 +0200 Subject: [PATCH 06/10] add template formatters --- framer/StyledText.coffee | 70 +++++++++++++++++++++++---------- framer/TextLayer.coffee | 7 ++++ test/tests/TextLayerTest.coffee | 24 +++++++++++ 3 files changed, 81 insertions(+), 20 deletions(-) diff --git a/framer/StyledText.coffee b/framer/StyledText.coffee index 0cbd5fb27..97346cdf1 100644 --- a/framer/StyledText.coffee +++ b/framer/StyledText.coffee @@ -396,37 +396,42 @@ class exports.StyledText textReplace: (search, replace) -> @blocks.map( (b) -> b.replaceText(search, replace)) - template: (data, list) -> + _buildTemplate: -> + # 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 + ) + # we store the initial template data, so template() can be called more than once - if not @_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 - ) - @_templateBlocks = @blocks.map((b) -> b.copy()) - else - # restore the original template - @blocks = @_templateBlocks.map((b) -> b.copy()) + @_templateBlocks = @blocks.map((b) -> b.copy()) + + template: (data, list) -> + @_buildTemplate() if not @_templateRanges + + # restore the original template + @blocks = @_templateBlocks.map((b) -> b.copy()) # replace a list of arguments in order if list if list.length > @_templateRanges.length list = list.slice(0, @_templateRanges.length) - list.reverse() # template ranges is in reverse order + # template ranges is in reverse order + list.reverse() first = @_templateRanges.length - list.length for range, index in @_templateRanges continue if index < first text = list[index - first] 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) return @@ -436,9 +441,34 @@ class exports.StyledText 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, list) -> + @_buildTemplate() if not @_templateRanges + + if list + if list.length > @_templateRanges.length + list = list.slice(0, @_templateRanges.length) + # template ranges is in reverse order + list.reverse() + first = @_templateRanges.length - list.length + for range, index in @_templateRanges + range.formatter = null # reset formatters if given this way + continue if index < first + formatter = list[index - first] + continue unless formatter? + continue unless _.isFunction(formatter) + range.formatter = formatter + return + return unless 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() diff --git a/framer/TextLayer.coffee b/framer/TextLayer.coffee index c5da9dafe..452253ae9 100644 --- a/framer/TextLayer.coffee +++ b/framer/TextLayer.coffee @@ -302,3 +302,10 @@ class exports.TextLayer extends Layer @_styledText.template(data, list) @renderText() @emit("change:text", @text) + + # data = {name: ()->(), ...} + templateFormatter: (data) -> + if _.isFunction(data) or not _.isObject(data) + list = _.concat([], arguments) + data = null + @_styledText.templateFormatter(data, list) diff --git a/test/tests/TextLayerTest.coffee b/test/tests/TextLayerTest.coffee index c531beb03..459ca8a42 100644 --- a/test/tests/TextLayerTest.coffee +++ b/test/tests/TextLayerTest.coffee @@ -89,6 +89,30 @@ describe "TextLayer.template", -> text.template() text.text.should.eql "{a}\n{b},{c}\n{d}" + it "should support formatters", -> + text = new TextLayer({text: "{report}"}) + text.templateFormatter + report: (v) -> v.toFixed(1) + text.template({report: 88.8121}) + text.text.should.eql "88.8" + + it "should support sugared formatters", -> + text = new TextLayer({text: "{report}"}) + text.templateFormatter (v) -> v.toFixed(1) + text.template 88.8122 + text.text.should.eql "88.8" + + it "should support multiplate formatters", -> + text = new TextLayer({text: "{title}\n{date} - {user}"}) + text.templateFormatter + date: (v) -> v.toISOString().slice(0, -8) + user: (v) -> v.toLowerCase() + text.template + title: "hello world" + date: new Date(1500000000000) + user: "Test User" + text.text.should.eql "hello world\n2017-07-14T02:40 - test user" + describe "TextLayer", -> describe "defaults", -> it "should set the correct defaults", -> From 71750472958fbd1af9d9e99bbf87364d56ea4d17 Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Wed, 26 Jul 2017 15:56:03 +0200 Subject: [PATCH 07/10] turn TextLayer.template and t.emplateFormatter into properties --- framer/TextLayer.coffee | 36 +++++++++++-------- test/tests/TextLayerTest.coffee | 62 +++++++++++++++++++-------------- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/framer/TextLayer.coffee b/framer/TextLayer.coffee index 452253ae9..53038c405 100644 --- a/framer/TextLayer.coffee +++ b/framer/TextLayer.coffee @@ -294,18 +294,24 @@ class exports.TextLayer extends Layer @renderText() @emit("change:text", @text) - # data = {name: "replacement text", ...} - template: (data) -> - if not _.isObject(data) - list = _.concat([], arguments) - data = null - @_styledText.template(data, list) - @renderText() - @emit("change:text", @text) - - # data = {name: ()->(), ...} - templateFormatter: (data) -> - if _.isFunction(data) or not _.isObject(data) - list = _.concat([], arguments) - data = null - @_styledText.templateFormatter(data, list) + @define "template", + get: -> @_templateData + set: (data) -> + @_templateData = data + if not _.isObject(data) + first = [data] + data = null + oldText = @text + @_styledText.template(data, first) + if @text isnt oldText + @renderText() + @emit("change:text", @text) + + @define "templateFormatter", + get: -> @_templateFormatter + set: (data) -> + @_templateFormatter = data + if _.isFunction(data) or not _.isObject(data) + first = [data] + data = null + @_styledText.templateFormatter(data, first) diff --git a/test/tests/TextLayerTest.coffee b/test/tests/TextLayerTest.coffee index 459ca8a42..037ced1c1 100644 --- a/test/tests/TextLayerTest.coffee +++ b/test/tests/TextLayerTest.coffee @@ -11,42 +11,43 @@ differentFonts = {"blocks": [{"inlineStyles": [{"startIndex": 0, "endIndex": 14, describe "TextLayer.template", -> it "should work", -> text = new TextLayer({text: "xxx {hello} xxx"}) - text.template({hello: "xxx"}) + text.template = {hello: "xxx"} text.text.should.eql "xxx xxx xxx" text._styledText.validate().should.equal true + text.template.hello.should.eql "xxx" - text.template({hello: "again"}) + text.template = {hello: "again"} text.text.should.eql "xxx again xxx" + text.textReplace("again", "HELLO THIS IS ME AND MORE") - text.template({hello: ""}) + text.template = {hello: ""} text.text.should.eql "xxx xxx" - text.template({hello: 42}) + text.template = {hello: 42} text.text.should.eql "xxx 42 xxx" - text.textReplace("again", "HELLO THIS IS ME AND MORE") text.text = "" text.text.should.eql "" - text.template({hello: "still works"}) + text.template = {hello: "still works"} text.text.should.eql "xxx still works xxx" text._styledText.validate().should.equal true it "should expand many blocks", -> text = new TextLayer({text: "{a},{b},{c},{d}"}) - text.template({a: "AAAAA"}) + text.template = {a: "AAAAA"} text.text.should.eql "AAAAA,{b},{c},{d}" - text.template({a: "AAAAA", b: "BEE"}) + text.template = {a: "AAAAA", b: "BEE"} text.text.should.eql "AAAAA,BEE,{c},{d}" - text.template({a: "AAAAA", b: "BEE", c: "CEEEE"}) + text.template = {a: "AAAAA", b: "BEE", c: "CEEEE"} text.text.should.eql "AAAAA,BEE,CEEEE,{d}" - text.template({a: "AAAAA", b: "BEE", c: "CEEEE", d: "DEE"}) + text.template = {a: "AAAAA", b: "BEE", c: "CEEEE", d: "DEE"} text.text.should.eql "AAAAA,BEE,CEEEE,DEE" text._styledText.validate().should.equal true it "should support multiple blocks and inline styles", -> text = new TextLayer({text: "{a}\n{b},{c}\n{d}"}) - text.template({a: "AAAAAAA", b: "BEE", c: "CEEEE", d: "DEE"}) + text.template = {a: "AAAAAAA", b: "BEE", c: "CEEEE", d: "DEE"} text.text.should.eql "AAAAAAA\nBEE,CEEEE\nDEE" text._styledText.validate().should.equal true @@ -54,64 +55,73 @@ describe "TextLayer.template", -> it "should support inserting multilines", -> text = new TextLayer({text: "{a}\n{b},{c}\n{d}"}) text._styledText.blocks.length.should.eql 3 - text.template({a: "ALONG", b: "BELOW\nMORE STUFF\nOEPS", c: "CIRCLE", d: "DEAF"}) + text.template = {a: "ALONG", b: "BELOW\nMORE STUFF\nOEPS", c: "CIRCLE", d: "DEAF"} text.text.should.eql "ALONG\nBELOW\nMORE STUFF\nOEPS,CIRCLE\nDEAF" text._styledText.validate().should.equal true # text._styledText.blocks.length.should.eql 5 it "should take a numbers, booleans", -> text = new TextLayer({text: "{a}"}) - text.template({a: 42}) + text.template = {a: 42} text.text.should.eql "42" - text.template({a: false}) + text.template = {a: false} text.text.should.eql "false" it "should not take null or nothing", -> text = new TextLayer({text: "{a}"}) - text.template({a: null}) + text.template = {a: null} text.text.should.eql "{a}" - text.template({b: "HELLO"}) + text.template = {b: "HELLO"} text.text.should.eql "{a}" it "should support no names sugar", -> text = new TextLayer({text: "{a}\n{b},{c}\n{d}"}) - text.template("A", "B", "C", 42) + text.template = "A" + text.text.should.eql "A\n{b},{c}\n{d}" + text.template = null + text.text.should.eql "{a}\n{b},{c}\n{d}" + + it "should support multiple lists using _styledText", -> + text = new TextLayer({text: "{a}\n{b},{c}\n{d}"}) + text._styledText.template(null, ["A", "B", "C", 42]) text.text.should.eql "A\nB,C\n42" - text.template("A", "B", "C", 42, "and", "too", "many") + text._styledText.template(null, ["A", "B", "C", 42, "and", "too", "many"]) text.text.should.eql "A\nB,C\n42" - text.template(true, 42) + text._styledText.template(null, [true, 42]) text.text.should.eql "true\n42,{c}\n{d}" - text.template() + text._styledText.template(null, []) text.text.should.eql "{a}\n{b},{c}\n{d}" it "should support formatters", -> text = new TextLayer({text: "{report}"}) - text.templateFormatter + text.templateFormatter = report: (v) -> v.toFixed(1) - text.template({report: 88.8121}) + text.template = + report: 88.8121 text.text.should.eql "88.8" it "should support sugared formatters", -> text = new TextLayer({text: "{report}"}) - text.templateFormatter (v) -> v.toFixed(1) - text.template 88.8122 + text.templateFormatter = (v) -> v.toFixed(1) + text.template = 88.8122 text.text.should.eql "88.8" it "should support multiplate formatters", -> text = new TextLayer({text: "{title}\n{date} - {user}"}) - text.templateFormatter + text.templateFormatter = date: (v) -> v.toISOString().slice(0, -8) user: (v) -> v.toLowerCase() - text.template + text.template = title: "hello world" date: new Date(1500000000000) user: "Test User" text.text.should.eql "hello world\n2017-07-14T02:40 - test user" + text.template.user.should.eql "Test User" describe "TextLayer", -> describe "defaults", -> From cbd8b613289e9fcef4331dc356932e48bc187cd3 Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Wed, 26 Jul 2017 15:56:41 +0200 Subject: [PATCH 08/10] make template values animatable --- framer/Animation.coffee | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/framer/Animation.coffee b/framer/Animation.coffee index b40e44be5..c4e62662a 100644 --- a/framer/Animation.coffee +++ b/framer/Animation.coffee @@ -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 @@ -338,6 +340,24 @@ 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) + if _.isNumber(toData) + toData = Utils.mapRange(value, 0, 1, parseFloat(fromData) or 0, toData) + @_target.template = toData + return + + for k, valueB of toData + if _.isNumber(valueB) + valueB = Utils.mapRange(value, 0, 1, parseFloat(fromData[k]) or 0, valueB) + targetData[k] = valueB + @_target[key] = targetData + _currentState: -> return _.pick(@layer, _.keys(@properties)) @@ -346,7 +366,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 From faf9208053504d31d87a37b3526f908b474e100f Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Wed, 26 Jul 2017 17:45:43 +0200 Subject: [PATCH 09/10] remember and merge values set by TextLayer.template = {}; fix animation --- framer/Animation.coffee | 18 +++++++--- framer/StyledText.coffee | 49 +++++----------------------- framer/TextLayer.coffee | 22 ++++++++----- test/tests/LayerAnimationTest.coffee | 39 ++++++++++++++++++++++ test/tests/TextLayerTest.coffee | 29 +++++----------- 5 files changed, 83 insertions(+), 74 deletions(-) diff --git a/framer/Animation.coffee b/framer/Animation.coffee index c4e62662a..d7b421721 100644 --- a/framer/Animation.coffee +++ b/framer/Animation.coffee @@ -347,16 +347,24 @@ class exports.Animation extends BaseClass targetData = {} if not _.isObject(toData) - if _.isNumber(toData) - toData = Utils.mapRange(value, 0, 1, parseFloat(fromData) or 0, toData) - @_target.template = 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) - valueB = Utils.mapRange(value, 0, 1, parseFloat(fromData[k]) or 0, 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[key] = targetData + @_target.template = targetData _currentState: -> return _.pick(@layer, _.keys(@properties)) diff --git a/framer/StyledText.coffee b/framer/StyledText.coffee index 97346cdf1..679233adb 100644 --- a/framer/StyledText.coffee +++ b/framer/StyledText.coffee @@ -396,7 +396,10 @@ class exports.StyledText textReplace: (search, replace) -> @blocks.map( (b) -> b.replaceText(search, replace)) - _buildTemplate: -> + # 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 = {} @@ -410,33 +413,17 @@ class exports.StyledText 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, list) -> - @_buildTemplate() if not @_templateRanges - + template: (data) -> # restore the original template @blocks = @_templateBlocks.map((b) -> b.copy()) - # replace a list of arguments in order - if list - if list.length > @_templateRanges.length - list = list.slice(0, @_templateRanges.length) - # template ranges is in reverse order - list.reverse() - first = @_templateRanges.length - list.length - for range, index in @_templateRanges - continue if index < first - text = list[index - first] - 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) - return - return unless data - # replace all ranges that are in data; @_templateRanges is reverse sorted, so ranges stay valid throughout for range in @_templateRanges text = data[range.name] @@ -445,25 +432,7 @@ class exports.StyledText block = @blocks[range.block] block.replaceRange(range.inline, range.start, range.length, text) - templateFormatter: (data, list) -> - @_buildTemplate() if not @_templateRanges - - if list - if list.length > @_templateRanges.length - list = list.slice(0, @_templateRanges.length) - # template ranges is in reverse order - list.reverse() - first = @_templateRanges.length - list.length - for range, index in @_templateRanges - range.formatter = null # reset formatters if given this way - continue if index < first - formatter = list[index - first] - continue unless formatter? - continue unless _.isFunction(formatter) - range.formatter = formatter - return - return unless data - + templateFormatter: (data) -> for range in @_templateRanges formatter = data[range.name] continue unless formatter? diff --git a/framer/TextLayer.coffee b/framer/TextLayer.coffee index 53038c405..764fd5e9f 100644 --- a/framer/TextLayer.coffee +++ b/framer/TextLayer.coffee @@ -294,15 +294,21 @@ class exports.TextLayer extends Layer @renderText() @emit("change:text", @text) + # we remember the template data, and merge it with new data @define "template", get: -> @_templateData set: (data) -> - @_templateData = data + if not @_templateData then @_templateData = {} + + firstName = @_styledText.buildTemplate() if not _.isObject(data) - first = [data] - data = null + return unless firstName + @_templateData[firstName] = data + else + _.assign(@_templateData, data) + oldText = @text - @_styledText.template(data, first) + @_styledText.template(@_templateData) if @text isnt oldText @renderText() @emit("change:text", @text) @@ -310,8 +316,8 @@ class exports.TextLayer extends Layer @define "templateFormatter", get: -> @_templateFormatter set: (data) -> - @_templateFormatter = data + firstName = @_styledText.buildTemplate() if _.isFunction(data) or not _.isObject(data) - first = [data] - data = null - @_styledText.templateFormatter(data, first) + return unless firstName + tmp = {}; tmp[firstName] = data; data = tmp + @_styledText.templateFormatter(data) diff --git a/test/tests/LayerAnimationTest.coffee b/test/tests/LayerAnimationTest.coffee index 292dcb6ec..1ef1e19c2 100644 --- a/test/tests/LayerAnimationTest.coffee +++ b/test/tests/LayerAnimationTest.coffee @@ -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() diff --git a/test/tests/TextLayerTest.coffee b/test/tests/TextLayerTest.coffee index 037ced1c1..dde67dbb7 100644 --- a/test/tests/TextLayerTest.coffee +++ b/test/tests/TextLayerTest.coffee @@ -33,16 +33,16 @@ describe "TextLayer.template", -> text.text.should.eql "xxx still works xxx" text._styledText.validate().should.equal true - it "should expand many blocks", -> + it "should expand many blocks, and remember old template values", -> text = new TextLayer({text: "{a},{b},{c},{d}"}) text.template = {a: "AAAAA"} text.text.should.eql "AAAAA,{b},{c},{d}" - text.template = {a: "AAAAA", b: "BEE"} + text.template = {b: "BEE"} text.text.should.eql "AAAAA,BEE,{c},{d}" - text.template = {a: "AAAAA", b: "BEE", c: "CEEEE"} + text.template = {c: "CEEEE"} text.text.should.eql "AAAAA,BEE,CEEEE,{d}" - text.template = {a: "AAAAA", b: "BEE", c: "CEEEE", d: "DEE"} - text.text.should.eql "AAAAA,BEE,CEEEE,DEE" + text.template = {a: "XXXX", d: "DEE"} + text.text.should.eql "XXXX,BEE,CEEEE,DEE" text._styledText.validate().should.equal true it "should support multiple blocks and inline styles", -> @@ -58,6 +58,7 @@ describe "TextLayer.template", -> text.template = {a: "ALONG", b: "BELOW\nMORE STUFF\nOEPS", c: "CIRCLE", d: "DEAF"} text.text.should.eql "ALONG\nBELOW\nMORE STUFF\nOEPS,CIRCLE\nDEAF" text._styledText.validate().should.equal true + # we don't actually build new blocks, or spans, not needed # text._styledText.blocks.length.should.eql 5 it "should take a numbers, booleans", -> @@ -76,27 +77,13 @@ describe "TextLayer.template", -> text.template = {b: "HELLO"} text.text.should.eql "{a}" - it "should support no names sugar", -> + it "should be able to set the first template without a name", -> text = new TextLayer({text: "{a}\n{b},{c}\n{d}"}) text.template = "A" text.text.should.eql "A\n{b},{c}\n{d}" text.template = null text.text.should.eql "{a}\n{b},{c}\n{d}" - it "should support multiple lists using _styledText", -> - text = new TextLayer({text: "{a}\n{b},{c}\n{d}"}) - text._styledText.template(null, ["A", "B", "C", 42]) - text.text.should.eql "A\nB,C\n42" - - text._styledText.template(null, ["A", "B", "C", 42, "and", "too", "many"]) - text.text.should.eql "A\nB,C\n42" - - text._styledText.template(null, [true, 42]) - text.text.should.eql "true\n42,{c}\n{d}" - - text._styledText.template(null, []) - text.text.should.eql "{a}\n{b},{c}\n{d}" - it "should support formatters", -> text = new TextLayer({text: "{report}"}) text.templateFormatter = @@ -105,7 +92,7 @@ describe "TextLayer.template", -> report: 88.8121 text.text.should.eql "88.8" - it "should support sugared formatters", -> + it "should support just setting the first formatter", -> text = new TextLayer({text: "{report}"}) text.templateFormatter = (v) -> v.toFixed(1) text.template = 88.8122 From 5d14d4dd9899a081e8351fc389ce4dca3d16682a Mon Sep 17 00:00:00 2001 From: Onne Gorter Date: Wed, 26 Jul 2017 17:52:22 +0200 Subject: [PATCH 10/10] skip WebFont loading tests by default --- test/tests/UtilsTest.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/tests/UtilsTest.coffee b/test/tests/UtilsTest.coffee index cfc4e9e3e..4c0819e9e 100644 --- a/test/tests/UtilsTest.coffee +++ b/test/tests/UtilsTest.coffee @@ -271,7 +271,7 @@ describe "Utils", -> # it "should return the right size with height constraint", -> # Utils.textSize(text, style, {height: 100}).should.eql(width: 168, height: 100) - describe "loadWebFontConfig", -> + describe.skip "loadWebFontConfig", -> describe "Real font loading tests", -> before -> # We skip the this test on CI, because I can't get the WebFont loading to work... :'( @@ -372,7 +372,7 @@ describe "Utils", -> done() return - describe "isFontFamilyLoaded", -> + describe.skip "isFontFamilyLoaded", -> it "should not reset the result if it is loaded successfully", (done) -> if mocha.env.CI @skip() @@ -400,7 +400,7 @@ describe "Utils", -> done() return - describe "loadWebFont", -> + describe.skip "loadWebFont", -> it "loads fonts at different weights" , -> raleway = Utils.loadWebFont("Raleway") raleway200 = Utils.loadWebFont("Raleway", 200)