enchanted_quill 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/lib/enchanted_quill/element_creator.rb +81 -0
- data/lib/enchanted_quill/gesture_recognizer_delegate_extension.rb +13 -0
- data/lib/enchanted_quill/label.rb +425 -0
- data/lib/enchanted_quill/parser.rb +25 -0
- data/lib/enchanted_quill/version.rb +3 -0
- data/lib/enchanted_quill.rb +11 -0
- metadata +68 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 5730b8d566d7ed7b273cddb1747f52854e5268da
|
|
4
|
+
data.tar.gz: 3393cc15dfcdab0aa9f6db26f507c0fa71601058
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cdcb55744c301335f38893f1fa0d69b397c2f4e5c0039c442ff7e3c209103c57d96e2ae824f5959e4f94e397e91bd3c965dd39d4bd94c32989f00126c44551ce
|
|
7
|
+
data.tar.gz: 014b262a9ac5814addab76c5101f967c551a2efe5ef8933edbdedbcc9045c52dcc79c3e36050de720f3a1063210d4618ff0f69088b10ff33a7cce95feafb5b48
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Contributor Code of Conduct
|
|
2
|
+
|
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
|
7
|
+
|
|
8
|
+
We are committed to making participation in this project a harassment-free
|
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
|
12
|
+
|
|
13
|
+
Examples of unacceptable behavior by participants include:
|
|
14
|
+
|
|
15
|
+
* The use of sexualized language or imagery
|
|
16
|
+
* Personal attacks
|
|
17
|
+
* Trolling or insulting/derogatory comments
|
|
18
|
+
* Public or private harassment
|
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
|
20
|
+
addresses, without explicit permission
|
|
21
|
+
* Other unethical or unprofessional conduct
|
|
22
|
+
|
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
27
|
+
threatening, offensive, or harmful.
|
|
28
|
+
|
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
|
32
|
+
Conduct may be permanently removed from the project team.
|
|
33
|
+
|
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
|
35
|
+
when an individual is representing the project or its community.
|
|
36
|
+
|
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
38
|
+
reported by contacting a project maintainer at gomezelom@yahoo.com. All
|
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
|
42
|
+
incident.
|
|
43
|
+
|
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
45
|
+
version 1.3.0, available at
|
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
|
47
|
+
|
|
48
|
+
[homepage]: http://contributor-covenant.org
|
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 Elom Gomez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# EnchantedQuill
|
|
2
|
+
|
|
3
|
+
**EnchantedQuill** is a rubymotion port of [ActiveLabel.swift](https://github.com/optonaut/ActiveLabel.swift), just like the maintainer of that gem
|
|
4
|
+
I had also tried different `cocoapods` that were drop in replacements of `UILabel` but they were either a bit complex to implement or just doing too much. I decided to go with [ActiveLabel.swift](https://github.com/optonaut/ActiveLabel.swift) because its API is cleaner and simpler to implement and understand.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
* Support for **Hashtags, Mentions, Categories and Links**
|
|
8
|
+
* Super easy to use and lightweight
|
|
9
|
+
* Acceptable test coverage :smile:
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
Add this line to your application's Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'enchanted_quill'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
And then execute:
|
|
21
|
+
|
|
22
|
+
$ bundle
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
label = EnchantedQuill::Label.alloc.init
|
|
28
|
+
label.text = text
|
|
29
|
+
label.textColor = UIColor.blackColor
|
|
30
|
+
label.font = UIFont.systemFontOfSize(12)
|
|
31
|
+
label.mention_color = UIColor.colorWithRed(85.0/255, green: 172.0/255, blue: 238.0/255, alpha: 1)
|
|
32
|
+
label.mention_selected_color = UIColor.colorWithRed(85.0/255, green: 172.0/255, blue: 238.0/255, alpha: 1).colorWithAlphaComponent(0.5)
|
|
33
|
+
label.hashtag_color = UIColor.colorWithRed(238.0/255, green: 85.0/255, blue: 96.0/255, alpha: 1)
|
|
34
|
+
label.hashtag_selected_color = UIColor.colorWithRed(238.0/255, green: 85.0/255, blue: 96.0/255, alpha: 1).colorWithAlphaComponent(0.5)
|
|
35
|
+
label.category_color = UIColor.brownColor
|
|
36
|
+
label.category_selected_color = UIColor.brownColor.colorWithAlphaComponent(0.5)
|
|
37
|
+
label.url_color = UIColor.colorWithRed(85.0/255, green: 238.0/255, blue: 151.0/255, alpha: 1)
|
|
38
|
+
label.url_selected_color = UIColor.colorWithRed(85.0/255, green: 238.0/255, blue: 151.0/255, alpha: 1).colorWithAlphaComponent(0.5)
|
|
39
|
+
|
|
40
|
+
label.handle_mention_tap do |mention|
|
|
41
|
+
p "Mention #{mention}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
label.handle_category_tap do |category|
|
|
45
|
+
p "Category #{category}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
label.handle_url_tap do |url|
|
|
49
|
+
p "URL #{url.absoluteString}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
label.handle_hashtag_tap do |hashtag|
|
|
53
|
+
p "Hashtag #{hashtag}"
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
## Better Performance
|
|
57
|
+
For better performance use the `customize` block, this basically groups all customizations on the label
|
|
58
|
+
and refreshes the textContainer once instead of refreshing each time an attribute is set.
|
|
59
|
+
```ruby
|
|
60
|
+
label.customize do
|
|
61
|
+
label.backgroundColor = UIColor.blackColor
|
|
62
|
+
label.text = text
|
|
63
|
+
label.textColor = UIColor.whiteColor
|
|
64
|
+
label.font = UIFont.systemFontOfSize(12)
|
|
65
|
+
label.numberOfLines = 0
|
|
66
|
+
label.mention_color = UIColor.colorWithRed(85.0/255, green: 172.0/255, blue: 238.0/255, alpha: 1)
|
|
67
|
+
label.mention_selected_color = UIColor.colorWithRed(85.0/255, green: 172.0/255, blue: 238.0/255, alpha: 1).colorWithAlphaComponent(0.5)
|
|
68
|
+
label.hashtag_color = UIColor.colorWithRed(238.0/255, green: 85.0/255, blue: 96.0/255, alpha: 1)
|
|
69
|
+
label.hashtag_selected_color = UIColor.colorWithRed(238.0/255, green: 85.0/255, blue: 96.0/255, alpha: 1).colorWithAlphaComponent(0.5)
|
|
70
|
+
label.category_color = UIColor.brownColor
|
|
71
|
+
label.category_selected_color = UIColor.brownColor.colorWithAlphaComponent(0.5)
|
|
72
|
+
label.url_color = UIColor.colorWithRed(85.0/255, green: 238.0/255, blue: 151.0/255, alpha: 1)
|
|
73
|
+
label.url_selected_color = UIColor.colorWithRed(85.0/255, green: 238.0/255, blue: 151.0/255, alpha: 1).colorWithAlphaComponent(0.5)
|
|
74
|
+
|
|
75
|
+
label.handle_mention_tap do |mention|
|
|
76
|
+
alert('Mention', mention)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
label.handle_category_tap do |category|
|
|
80
|
+
alert('Category', category)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
label.handle_url_tap do |url|
|
|
84
|
+
alert('URL', url.absoluteString)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
label.handle_hashtag_tap do |hashtag|
|
|
88
|
+
alert('HashTag', hashtag)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
label.adjustsFontSizeToFitWidth = true
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
## Credits
|
|
97
|
+
A special thanks to [@optonaut](https://github.com/optonaut/ActiveLabel.swift) for implementing such an easy to use pod for swift. :+1:
|
|
98
|
+
|
|
99
|
+
## Contributing
|
|
100
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/paddingtonsbear/enchanted_quill. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
105
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module EnchantedQuill
|
|
2
|
+
class ElementCreator
|
|
3
|
+
Element = Struct.new(:type, :word, :range)
|
|
4
|
+
|
|
5
|
+
def self.create_mentions_elements(text, range)
|
|
6
|
+
mentions = EnchantedQuill::Parser.parse_elements_for(:mention, text, range)
|
|
7
|
+
elements = []
|
|
8
|
+
|
|
9
|
+
mentions.each do |mention|
|
|
10
|
+
if mention.range.length > 2
|
|
11
|
+
range = NSMakeRange(mention.range.location + 1, mention.range.length - 1)
|
|
12
|
+
word = text.substringWithRange(range)
|
|
13
|
+
|
|
14
|
+
if word[0] == '@'
|
|
15
|
+
word = word[1..-1]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
elements << Element.new(:mention, word, range)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
elements
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.create_hashtag_elements(text, range)
|
|
26
|
+
hashtags = EnchantedQuill::Parser.parse_elements_for(:hashtag, text, range)
|
|
27
|
+
elements = []
|
|
28
|
+
|
|
29
|
+
hashtags.each do |hashtag|
|
|
30
|
+
if hashtag.range.length > 2
|
|
31
|
+
range = NSMakeRange(hashtag.range.location + 1, hashtag.range.length - 1)
|
|
32
|
+
word = text.substringWithRange(range)
|
|
33
|
+
|
|
34
|
+
if word[0] == '#'
|
|
35
|
+
word = word[1..-1]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
elements << Element.new(:hashtag, word, range)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
elements
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.create_category_elements(text, range)
|
|
46
|
+
categories = EnchantedQuill::Parser.parse_elements_for(:category, text, range)
|
|
47
|
+
elements = []
|
|
48
|
+
|
|
49
|
+
categories.each do |category|
|
|
50
|
+
if category.range.length > 2
|
|
51
|
+
range = NSMakeRange(category.range.location + 1, category.range.length - 1)
|
|
52
|
+
word = text.substringWithRange(range)
|
|
53
|
+
|
|
54
|
+
if word[0] == '{' && word[-1] == '}'
|
|
55
|
+
word = word[1..-2]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
elements << Element.new(:category, word, range)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
elements
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.create_url_elements(text, range)
|
|
66
|
+
urls = EnchantedQuill::Parser.parse_elements_for(:url, text, range)
|
|
67
|
+
elements = []
|
|
68
|
+
|
|
69
|
+
urls.each do |url|
|
|
70
|
+
if url.range.length > 2
|
|
71
|
+
word = text.substringWithRange(url.range)
|
|
72
|
+
.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
|
73
|
+
|
|
74
|
+
elements << Element.new(:url, word, url.range)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
elements
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class UIGestureRecognizerDelegate
|
|
2
|
+
def gestureRecognizer(gesture_recognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer: other_gesture_recognizer)
|
|
3
|
+
true
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def gestureRecognizer(gesture_recognizer, shouldRequireFailureOfGestureRecognizer: other_gesture_recognizer)
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def gestureRecognizer(gesture_recognizer, shouldBeRequiredToFailByGestureRecognizer: other_gesture_recognizer)
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
module EnchantedQuill
|
|
2
|
+
class Label < UILabel
|
|
3
|
+
attr_reader :mention_tap_handler, :url_tap_handler, :hashtag_tap_handler,
|
|
4
|
+
:category_tap_handler, :mention_color, :mention_selected_color,
|
|
5
|
+
:hashtag_color, :hashtag_selected_color, :category_color,
|
|
6
|
+
:category_selected_color, :url_color, :url_selected_color,
|
|
7
|
+
:line_spacing
|
|
8
|
+
|
|
9
|
+
def init
|
|
10
|
+
super.tap do |label|
|
|
11
|
+
label.setup_label
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initWithFrame(frame)
|
|
16
|
+
super.tap do |label|
|
|
17
|
+
label.setup_label
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def drawTextInRect(rect)
|
|
22
|
+
range = NSMakeRange(0, text_storage.length)
|
|
23
|
+
text_container.size = rect.size
|
|
24
|
+
new_origin = text_origin(rect)
|
|
25
|
+
|
|
26
|
+
layout_manager.drawBackgroundForGlyphRange(range, atPoint: new_origin)
|
|
27
|
+
layout_manager.drawGlyphsForGlyphRange(range, atPoint: new_origin)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def customize(&block)
|
|
31
|
+
@customizing = true
|
|
32
|
+
block.call(self)
|
|
33
|
+
@customizing = false
|
|
34
|
+
update_text_storage
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on_touch(touch)
|
|
39
|
+
location = touch.locationInView(self)
|
|
40
|
+
avoid_super_call = false
|
|
41
|
+
|
|
42
|
+
case touch.phase
|
|
43
|
+
when UITouchPhaseBegan, UITouchPhaseMoved
|
|
44
|
+
if element = element_at_location(location)
|
|
45
|
+
if element.range.location != (selected_element && selected_element.range.location) ||
|
|
46
|
+
element.range.length != (selected_element && selected_element.range.length)
|
|
47
|
+
|
|
48
|
+
update_attributed_when_selected(false)
|
|
49
|
+
@selected_element = element
|
|
50
|
+
update_attributed_when_selected(true)
|
|
51
|
+
end
|
|
52
|
+
avoid_super_call = true
|
|
53
|
+
else
|
|
54
|
+
update_attributed_when_selected(false)
|
|
55
|
+
@selected_element = nil
|
|
56
|
+
end
|
|
57
|
+
when UITouchPhaseEnded
|
|
58
|
+
return avoid_super_call unless selected_element
|
|
59
|
+
|
|
60
|
+
case selected_element.type
|
|
61
|
+
when :category then did_tap_category(selected_element.word)
|
|
62
|
+
when :mention then did_tap_mention(selected_element.word)
|
|
63
|
+
when :hashtag then did_tap_hashtag(selected_element.word)
|
|
64
|
+
when :url then did_tap_string_url(selected_element.word)
|
|
65
|
+
else
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Dispatch::Queue.concurrent.after(0.1) do
|
|
69
|
+
Dispatch::Queue.main.async do
|
|
70
|
+
update_attributed_when_selected(false)
|
|
71
|
+
@selected_element = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
avoid_super_call = true
|
|
75
|
+
when UITouchPhaseCancelled
|
|
76
|
+
@selected_element = nil
|
|
77
|
+
when UITouchPhaseStationary
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
avoid_super_call
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def delegate=(delegate)
|
|
84
|
+
@delegate = WeakRef.new(delegate)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def delegate
|
|
88
|
+
@delegate
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def handle_mention_tap(&block)
|
|
92
|
+
@mention_tap_handler = block
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def handle_hashtag_tap(&block)
|
|
96
|
+
@hashtag_tap_handler = block
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def handle_url_tap(&block)
|
|
100
|
+
@url_tap_handler = block
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_category_tap(&block)
|
|
104
|
+
@category_tap_handler = block
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# override UILabel attributes
|
|
108
|
+
def text=(text)
|
|
109
|
+
super
|
|
110
|
+
update_text_storage
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def attributedText=(attributed_text)
|
|
114
|
+
super
|
|
115
|
+
update_text_storage
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def font=(font)
|
|
119
|
+
super
|
|
120
|
+
update_text_storage(false)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def textColor=(color)
|
|
124
|
+
super
|
|
125
|
+
update_text_storage(false)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def textAlignment=(alignment)
|
|
129
|
+
super
|
|
130
|
+
update_text_storage(false)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def mention_color=(color)
|
|
134
|
+
@mention_color = color
|
|
135
|
+
update_text_storage(false)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def mention_selected_color=(color)
|
|
139
|
+
@mention_selected_color = color
|
|
140
|
+
update_text_storage(false)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def hashtag_color=(color)
|
|
144
|
+
@hashtag_color = color
|
|
145
|
+
update_text_storage(false)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def hashtag_selected_color=(color)
|
|
149
|
+
@hashtag_selected_color = color
|
|
150
|
+
update_text_storage(false)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def category_color=(color)
|
|
154
|
+
@category_color = color
|
|
155
|
+
update_text_storage(false)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def category_selected_color=(color)
|
|
159
|
+
@category_selected_color = color
|
|
160
|
+
update_text_storage(false)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def url_color=(color)
|
|
164
|
+
@url_color = color
|
|
165
|
+
update_text_storage(false)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def url_selected_color=(color)
|
|
169
|
+
@url_selected_color = color
|
|
170
|
+
update_text_storage(false)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def line_spacing=(spacing)
|
|
174
|
+
@line_spacing = spacing
|
|
175
|
+
update_text_storage(false)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def touchesBegan(touches, withEvent: event)
|
|
179
|
+
touches = touches && touches.allObjects
|
|
180
|
+
touch = touches.first
|
|
181
|
+
return unless touch
|
|
182
|
+
return if on_touch(touch)
|
|
183
|
+
super
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def touchesCancelled(touches, withEvent: event)
|
|
187
|
+
touches = touches && touches.allObjects
|
|
188
|
+
touch = touches.first
|
|
189
|
+
return unless touch
|
|
190
|
+
on_touch(touch)
|
|
191
|
+
super
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def touchesEnded(touches, withEvent: event)
|
|
195
|
+
touches = touches && touches.allObjects
|
|
196
|
+
touch = touches.first
|
|
197
|
+
return unless touch
|
|
198
|
+
return if on_touch(touch)
|
|
199
|
+
super
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# def intrinsicContentSize
|
|
203
|
+
# label_height = sizeThatFits(CGSizeMake(CGRectGetWidth(self.frame), Float::MAX)).height
|
|
204
|
+
# return CGSizeMake(self.frame.size.width, label_height + self.layoutMargins.top + self.layoutMargins.bottom)
|
|
205
|
+
# end
|
|
206
|
+
|
|
207
|
+
def setup_label
|
|
208
|
+
@customizing = false
|
|
209
|
+
@active_elements = {}
|
|
210
|
+
self.textColor = UIColor.blackColor
|
|
211
|
+
self.mention_color = UIColor.blueColor
|
|
212
|
+
self.mention_selected_color = UIColor.blueColor.colorWithAlphaComponent(0.5)
|
|
213
|
+
self.hashtag_color = UIColor.blueColor
|
|
214
|
+
self.hashtag_selected_color = UIColor.blueColor.colorWithAlphaComponent(0.5)
|
|
215
|
+
self.category_color = UIColor.brownColor
|
|
216
|
+
self.category_selected_color = UIColor.brownColor.colorWithAlphaComponent(0.5)
|
|
217
|
+
self.url_color = UIColor.blueColor
|
|
218
|
+
self.url_selected_color = UIColor.blueColor.colorWithAlphaComponent(0.5)
|
|
219
|
+
|
|
220
|
+
text_storage.addLayoutManager(layout_manager)
|
|
221
|
+
layout_manager.addTextContainer(text_container)
|
|
222
|
+
text_container.lineFragmentPadding = 0
|
|
223
|
+
self.userInteractionEnabled = true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
attr_reader :customizing, :selected_element, :mention_tap_handler,
|
|
229
|
+
:url_tap_handler, :hashtag_tap_handler, :category_tap_handler,
|
|
230
|
+
:height_correction, :active_elements
|
|
231
|
+
|
|
232
|
+
def layout_manager
|
|
233
|
+
@layout_manager ||= NSLayoutManager.alloc.init
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def text_container
|
|
237
|
+
@text_container ||= NSTextContainer.alloc.init
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def text_storage
|
|
241
|
+
@text_storage ||= NSTextStorage.alloc.init
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def update_text_storage(parse_text = true)
|
|
245
|
+
return if customizing
|
|
246
|
+
|
|
247
|
+
attributed_text = attributedText
|
|
248
|
+
if attributed_text.nil? || attributed_text.length == 0
|
|
249
|
+
text_storage.setAttributedString(NSAttributedString.alloc.init)
|
|
250
|
+
return
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
mut_attr_string = add_line_break(attributed_text)
|
|
254
|
+
|
|
255
|
+
if parse_text
|
|
256
|
+
@selected_element = nil
|
|
257
|
+
active_elements.each do |type, _|
|
|
258
|
+
@active_elements[type] = []
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
parse_text_and_extract_active_elements(mut_attr_string)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
add_link_attribute(mut_attr_string)
|
|
265
|
+
text_storage.setAttributedString(mut_attr_string)
|
|
266
|
+
setNeedsDisplay
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def text_origin(rect)
|
|
270
|
+
used_rect = layout_manager.usedRectForTextContainer(text_container)
|
|
271
|
+
@height_correction = (rect.size.height - used_rect.size.height) / 2
|
|
272
|
+
glyph_origin_y = height_correction > 0 ? rect.origin.y + height_correction : rect.origin.y
|
|
273
|
+
CGPointMake(rect.origin.x, glyph_origin_y)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# add link attribute
|
|
277
|
+
def add_link_attribute(mut_attr_string)
|
|
278
|
+
range_pointer = Pointer.new(NSRange.type)
|
|
279
|
+
attributes = mut_attr_string.attributesAtIndex(0, effectiveRange: range_pointer).dup
|
|
280
|
+
|
|
281
|
+
attributes[NSFontAttributeName] = self.font
|
|
282
|
+
attributes[NSForegroundColorAttributeName] = self.textColor
|
|
283
|
+
|
|
284
|
+
mut_attr_string.addAttributes(attributes, range: range_pointer[0])
|
|
285
|
+
|
|
286
|
+
attributes[NSForegroundColorAttributeName] = mention_color
|
|
287
|
+
|
|
288
|
+
active_elements.each do |type, elements|
|
|
289
|
+
case type
|
|
290
|
+
when :mention then attributes[NSForegroundColorAttributeName] = mention_color
|
|
291
|
+
when :hashtag then attributes[NSForegroundColorAttributeName] = hashtag_color
|
|
292
|
+
when :url then attributes[NSForegroundColorAttributeName] = url_color
|
|
293
|
+
when :category then attributes[NSForegroundColorAttributeName] = category_color
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
elements.each do |element|
|
|
297
|
+
mut_attr_string.setAttributes(attributes, range: element.range)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def parse_text_and_extract_active_elements(attr_string)
|
|
303
|
+
text_string = attr_string.string
|
|
304
|
+
text_length = text_string.length
|
|
305
|
+
text_range = NSMakeRange(0, text_length)
|
|
306
|
+
|
|
307
|
+
# urls
|
|
308
|
+
url_elements = ElementCreator.create_url_elements(text_string, text_range)
|
|
309
|
+
@active_elements[:url] = url_elements
|
|
310
|
+
|
|
311
|
+
# hash_tags
|
|
312
|
+
hashtag_elements = ElementCreator.create_hashtag_elements(text_string, text_range)
|
|
313
|
+
@active_elements[:hashtag] = hashtag_elements
|
|
314
|
+
|
|
315
|
+
# mentions
|
|
316
|
+
mention_elements = ElementCreator.create_mentions_elements(text_string, text_range)
|
|
317
|
+
@active_elements[:mention] = mention_elements
|
|
318
|
+
|
|
319
|
+
# categories
|
|
320
|
+
category_elements = ElementCreator.create_category_elements(text_string, text_range)
|
|
321
|
+
@active_elements[:category] = category_elements
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def add_line_break(attr_string)
|
|
325
|
+
mut_attr_string = NSMutableAttributedString.alloc.initWithAttributedString(attr_string)
|
|
326
|
+
|
|
327
|
+
range_pointer = Pointer.new(NSRange.type)
|
|
328
|
+
attributes = mut_attr_string.attributesAtIndex(0, effectiveRange: range_pointer).dup
|
|
329
|
+
|
|
330
|
+
current_paragraph_style = attributes[NSParagraphStyleAttributeName] && attributes[NSParagraphStyleAttributeName].mutableCopy
|
|
331
|
+
paragraph_style = current_paragraph_style || NSMutableParagraphStyle.alloc.init
|
|
332
|
+
paragraph_style.lineBreakMode = NSLineBreakByWordWrapping
|
|
333
|
+
paragraph_style.alignment = textAlignment
|
|
334
|
+
|
|
335
|
+
if line_spacing
|
|
336
|
+
paragraph_style.lineSpacing = line_spacing
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
attributes[NSParagraphStyleAttributeName] = paragraph_style
|
|
340
|
+
mut_attr_string.setAttributes(attributes, range: range_pointer[0])
|
|
341
|
+
mut_attr_string
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def update_attributed_when_selected(selected)
|
|
345
|
+
return unless @selected_element
|
|
346
|
+
|
|
347
|
+
attributes = text_storage.attributesAtIndex(0, effectiveRange: nil).dup
|
|
348
|
+
unless selected
|
|
349
|
+
case @selected_element.type
|
|
350
|
+
when :category then attributes[NSForegroundColorAttributeName] = category_color
|
|
351
|
+
when :hashtag then attributes[NSForegroundColorAttributeName] = hashtag_color
|
|
352
|
+
when :mention then attributes[NSForegroundColorAttributeName] = mention_color
|
|
353
|
+
when :url then attributes[NSForegroundColorAttributeName] = url_color
|
|
354
|
+
end
|
|
355
|
+
else
|
|
356
|
+
case @selected_element.type
|
|
357
|
+
when :category then attributes[NSForegroundColorAttributeName] = category_selected_color || category_color
|
|
358
|
+
when :hashtag then attributes[NSForegroundColorAttributeName] = hashtag_selected_color || hashtag_color
|
|
359
|
+
when :mention then attributes[NSForegroundColorAttributeName] = mention_selected_color || mention_color
|
|
360
|
+
when :url then attributes[NSForegroundColorAttributeName] = url_selected_color || url_color
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
text_storage.addAttributes(attributes, range: @selected_element.range)
|
|
365
|
+
setNeedsDisplay
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def element_at_location(location)
|
|
369
|
+
return unless text_storage.length > 0
|
|
370
|
+
|
|
371
|
+
y = location.y - height_correction
|
|
372
|
+
correct_location = CGPointMake(location.x, y)
|
|
373
|
+
bounding_rect = layout_manager.boundingRectForGlyphRange(NSMakeRange(0, text_storage.length), inTextContainer: text_container)
|
|
374
|
+
|
|
375
|
+
return unless CGRectContainsPoint(bounding_rect, correct_location)
|
|
376
|
+
|
|
377
|
+
index = layout_manager.glyphIndexForPoint(correct_location, inTextContainer: text_container)
|
|
378
|
+
|
|
379
|
+
active_element = nil
|
|
380
|
+
active_elements.values.flatten.each do |element|
|
|
381
|
+
if index < text_storage.length - 1 &&
|
|
382
|
+
index >= element.range.location &&
|
|
383
|
+
index < element.range.location + element.range.length
|
|
384
|
+
active_element = element
|
|
385
|
+
break
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
active_element
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def did_tap_mention(username)
|
|
393
|
+
if mention_tap_handler
|
|
394
|
+
mention_tap_handler.call(username)
|
|
395
|
+
elsif delegate && delegate.respond_to?('did_select_text:type:')
|
|
396
|
+
delegate.did_select_text(username, type: :mention)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def did_tap_hashtag(hashtag)
|
|
401
|
+
if hashtag_tap_handler
|
|
402
|
+
hashtag_tap_handler.call(hashtag)
|
|
403
|
+
elsif delegate && delegate.respond_to?('did_select_text:type:')
|
|
404
|
+
delegate.did_select_text(hashtag, type: :hashtag)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def did_tap_string_url(url)
|
|
409
|
+
url = NSURL.URLWithString(url)
|
|
410
|
+
if url_tap_handler
|
|
411
|
+
url_tap_handler.call(url)
|
|
412
|
+
elsif delegate && delegate.respond_to?('did_select_text:type:')
|
|
413
|
+
delegate.did_select_text(url, type: :url)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def did_tap_category(category)
|
|
418
|
+
if category_tap_handler
|
|
419
|
+
category_tap_handler.call(category)
|
|
420
|
+
elsif delegate && delegate.respond_to?('did_select_text:type:')
|
|
421
|
+
delegate.did_select_text(category, type: :category)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module EnchantedQuill
|
|
2
|
+
class Parser
|
|
3
|
+
CATEGORY_REGEX = "(?:^|\\s|$)(\\{.*?\\})"
|
|
4
|
+
HASHTAG_REGEX = "(?:^|\\s|$)#[\\p{L}0-9_]*"
|
|
5
|
+
MENTION_REGEX = "(?:^|\\s|$|[.])@[\\p{L}0-9_]*"
|
|
6
|
+
URL_REGEX = "(^|[\\s.:;?\\-\\]<\\(])" +
|
|
7
|
+
"((https?://|www.|pic.)[-\\w;/?:@&=+$\\|\\_.!~*\\|'()\\[\\]%#,☺]+[\\w/#](\\(\\))?)" +
|
|
8
|
+
"(?=$|[\\s',\\|\\(\\).:;?\\-\\[\\]>\\)])"
|
|
9
|
+
|
|
10
|
+
TYPE_WITH_REGEX = {
|
|
11
|
+
url: URL_REGEX,
|
|
12
|
+
hashtag: HASHTAG_REGEX,
|
|
13
|
+
mention: MENTION_REGEX,
|
|
14
|
+
category: CATEGORY_REGEX
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def self.parse_elements_for(type, text, range)
|
|
18
|
+
regex = TYPE_WITH_REGEX[type]
|
|
19
|
+
return [] unless regex
|
|
20
|
+
|
|
21
|
+
regex = NSRegularExpression.alloc.initWithPattern(regex, options: NSRegularExpressionCaseInsensitive, error: nil)
|
|
22
|
+
regex.matchesInString(text, options: 0, range: range)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require "enchanted_quill/version"
|
|
2
|
+
|
|
3
|
+
unless defined?(Motion::Project::App)
|
|
4
|
+
raise "This must be required from within a RubyMotion Rakefile"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
Motion::Project::App.setup do |app|
|
|
8
|
+
lib_dir = File.join(File.dirname(__FILE__))
|
|
9
|
+
app.files.unshift(Dir.glob(File.join(lib_dir, "enchanted_quill/**/*.rb")))
|
|
10
|
+
app.frameworks += %w{ Foundation UIKit }
|
|
11
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: enchanted_quill
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Elom Gomez
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2016-04-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description: Drop in replacement for UILabel with support for mentions, links and
|
|
28
|
+
hashtags for RubyMotion
|
|
29
|
+
email:
|
|
30
|
+
- gomezelom@yahoo.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CODE_OF_CONDUCT.md
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- lib/enchanted_quill.rb
|
|
39
|
+
- lib/enchanted_quill/element_creator.rb
|
|
40
|
+
- lib/enchanted_quill/gesture_recognizer_delegate_extension.rb
|
|
41
|
+
- lib/enchanted_quill/label.rb
|
|
42
|
+
- lib/enchanted_quill/parser.rb
|
|
43
|
+
- lib/enchanted_quill/version.rb
|
|
44
|
+
homepage: https://github.com/paddingtonsbear/enchanted_quill
|
|
45
|
+
licenses:
|
|
46
|
+
- MIT
|
|
47
|
+
metadata: {}
|
|
48
|
+
post_install_message:
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '0'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubyforge_project:
|
|
64
|
+
rubygems_version: 2.6.2
|
|
65
|
+
signing_key:
|
|
66
|
+
specification_version: 4
|
|
67
|
+
summary: RubyMotion port of ActiveLabel.swift
|
|
68
|
+
test_files: []
|