hashtags 0.1.0
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/.gitignore +9 -0
- data/.travis.yml +25 -0
- data/Gemfile +33 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +253 -0
- data/Rakefile +19 -0
- data/app/controllers/hashtags/resources_controller.rb +28 -0
- data/app/helpers/hashtags/view_helper.rb +8 -0
- data/app/views/hashtags/_help.slim +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/locales/.gitkeep +0 -0
- data/config/locales/hashtags/builder.en.yml +5 -0
- data/config/routes.rb +5 -0
- data/hashtags.gemspec +35 -0
- data/lib/assets/javascripts/hashtags.js +3 -0
- data/lib/assets/javascripts/hashtags/hashtags.coffee +66 -0
- data/lib/assets/stylesheets/hashtags.css +3 -0
- data/lib/assets/stylesheets/hashtags/help.scss +11 -0
- data/lib/assets/stylesheets/hashtags/textcomplete.scss +21 -0
- data/lib/hashtags.rb +11 -0
- data/lib/hashtags/base.rb +161 -0
- data/lib/hashtags/builder.rb +81 -0
- data/lib/hashtags/engine.rb +7 -0
- data/lib/hashtags/mongoid_extension.rb +36 -0
- data/lib/hashtags/resource.rb +92 -0
- data/lib/hashtags/resource_type.rb +56 -0
- data/lib/hashtags/user.rb +92 -0
- data/lib/hashtags/variable.rb +59 -0
- data/lib/hashtags/version.rb +3 -0
- metadata +243 -0
data/bin/setup
ADDED
File without changes
|
data/config/routes.rb
ADDED
data/hashtags.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hashtags/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'hashtags'
|
8
|
+
spec.version = Hashtags::VERSION
|
9
|
+
spec.authors = ['Tomas Celizna']
|
10
|
+
spec.email = ['tomas.celizna@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'Rails engine to facilitate inline text hashtags.'
|
13
|
+
spec.homepage = 'https://github.com/tomasc/hashtags'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = 'exe'
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'handlebars'
|
22
|
+
spec.add_dependency 'rails', '~> 4'
|
23
|
+
spec.add_dependency 'rails-assets-jquery-textcomplete'
|
24
|
+
spec.add_dependency 'rails-assets-handlebars'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
27
|
+
spec.add_development_dependency 'guard'
|
28
|
+
spec.add_development_dependency 'guard-minitest'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
31
|
+
|
32
|
+
spec.add_development_dependency 'database_cleaner'
|
33
|
+
spec.add_development_dependency 'mongoid', '~> 5.0'
|
34
|
+
spec.add_development_dependency 'slim'
|
35
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#= require handlebars
|
2
|
+
#= require jquery-textcomplete
|
3
|
+
|
4
|
+
# https://github.com/zenorocha/jquery-boilerplate
|
5
|
+
(($, window) ->
|
6
|
+
pluginName = 'hashtags'
|
7
|
+
document = window.document
|
8
|
+
|
9
|
+
defaults =
|
10
|
+
debug: false
|
11
|
+
|
12
|
+
class Plugin
|
13
|
+
constructor: (@element, options) ->
|
14
|
+
@options = $.extend {}, defaults, options
|
15
|
+
|
16
|
+
@$element = $(@element)
|
17
|
+
|
18
|
+
@_defaults = defaults
|
19
|
+
@_name = pluginName
|
20
|
+
|
21
|
+
@init()
|
22
|
+
|
23
|
+
init: ->
|
24
|
+
@$element.textcomplete(@get_strategies())
|
25
|
+
|
26
|
+
get_data: -> @$element.data 'hashtags'
|
27
|
+
get_default_path: -> @get_data()['path']
|
28
|
+
get_strategies: -> $.map @get_data()['strategies'], (strategy) => @get_strategy(strategy)
|
29
|
+
get_strategy: (data) ->
|
30
|
+
{
|
31
|
+
index: data.match_index
|
32
|
+
cache: true
|
33
|
+
context: (text) -> text.toLowerCase()
|
34
|
+
match: ///#{data.match_regexp}///
|
35
|
+
search: (term, callback, match) =>
|
36
|
+
if data.values then @search_values(data, term, callback, match)
|
37
|
+
else @search_remote(data, term, callback, match)
|
38
|
+
replace: (resource, event) -> Handlebars.compile(data.replace)(resource)
|
39
|
+
template: (resource, term) -> Handlebars.compile(data.template)(resource)
|
40
|
+
}
|
41
|
+
|
42
|
+
search_values: (data, term, callback, match) ->
|
43
|
+
callback $.map( data.values, (value) ->
|
44
|
+
if value.match(///^#{term}///i) then value else null
|
45
|
+
)
|
46
|
+
|
47
|
+
search_remote: (data, term, callback, match) ->
|
48
|
+
url = data.path ? @get_default_path()
|
49
|
+
match_template = data.match_template
|
50
|
+
|
51
|
+
$.getJSON( url , q: term, class_name: data.class_name )
|
52
|
+
.done( (resp) ->
|
53
|
+
callback $.map( resp, (resource) ->
|
54
|
+
if Handlebars.compile(match_template)(resource).match(///#{term}///i) then resource else null
|
55
|
+
)
|
56
|
+
)
|
57
|
+
.fail -> callback []
|
58
|
+
|
59
|
+
$.fn[pluginName] = (options) ->
|
60
|
+
@each ->
|
61
|
+
if !$.data(this, "plugin_#{pluginName}")
|
62
|
+
$.data(@, "plugin_#{pluginName}", new Plugin(@, options))
|
63
|
+
|
64
|
+
)(jQuery, window)
|
65
|
+
|
66
|
+
$ -> $('input[type="text"][data-hashtags], textarea[data-hashtags]').hashtags()
|
@@ -0,0 +1,21 @@
|
|
1
|
+
ul.textcomplete-dropdown {
|
2
|
+
background-color: white;
|
3
|
+
border: 1px solid;
|
4
|
+
list-style: none;
|
5
|
+
margin: 0;
|
6
|
+
padding: 0;
|
7
|
+
|
8
|
+
li.textcomplete-item {
|
9
|
+
border-radius: 0;
|
10
|
+
margin: 0;
|
11
|
+
padding: .25em .5em;
|
12
|
+
&:not(:last-child) {
|
13
|
+
border-bottom: 1px solid;
|
14
|
+
}
|
15
|
+
|
16
|
+
&:hover, &.active {
|
17
|
+
cursor: pointer;
|
18
|
+
background-color: yellow;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
data/lib/hashtags.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'hashtags/base'
|
2
|
+
require 'hashtags/builder'
|
3
|
+
|
4
|
+
require 'hashtags/mongoid_extension'
|
5
|
+
require 'hashtags/resource_type'
|
6
|
+
require 'hashtags/resource'
|
7
|
+
require 'hashtags/user'
|
8
|
+
require 'hashtags/variable'
|
9
|
+
|
10
|
+
require 'hashtags/engine'
|
11
|
+
require 'hashtags/version'
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module Hashtags
|
2
|
+
# Base class, from which all Hashtag types inherit
|
3
|
+
# In case you wish to add a new type next to
|
4
|
+
# • ResourceType
|
5
|
+
# • Resource
|
6
|
+
# • User
|
7
|
+
# • Variable
|
8
|
+
# you might want to inherit from Base.
|
9
|
+
|
10
|
+
class Base < Struct.new(:str)
|
11
|
+
def self.to_markup(str)
|
12
|
+
new(str).to_markup
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.to_hashtag(str)
|
16
|
+
new(str).to_hashtag
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.descendants
|
20
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.resource_classes
|
24
|
+
Resource.descendants
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.user_classes
|
28
|
+
User.descendants
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.variable_classes
|
32
|
+
Variable.descendants
|
33
|
+
end
|
34
|
+
|
35
|
+
# to be passed to textcomplete
|
36
|
+
def self.strategy(hashtag_classes)
|
37
|
+
{
|
38
|
+
class_name: to_s,
|
39
|
+
match_regexp: json_regexp(match_regexp),
|
40
|
+
match_index: match_index,
|
41
|
+
match_template: match_template,
|
42
|
+
path: path,
|
43
|
+
replace: replace,
|
44
|
+
template: template,
|
45
|
+
values: values(hashtag_classes)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# for example @ # $ …
|
50
|
+
def self.trigger
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.match_template
|
55
|
+
end
|
56
|
+
|
57
|
+
# used to expire field with tags
|
58
|
+
def self.cache_key
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
|
62
|
+
# regexp to recognize the complete tag
|
63
|
+
def self.regexp
|
64
|
+
raise NotImplementedError
|
65
|
+
end
|
66
|
+
|
67
|
+
# implement to use custom controller
|
68
|
+
def self.path
|
69
|
+
end
|
70
|
+
|
71
|
+
# implement to preload hash tag values,
|
72
|
+
# fe in case there is limited number (ie variable names)
|
73
|
+
def self.values(hashtag_classes)
|
74
|
+
end
|
75
|
+
|
76
|
+
# implement to show which values particular trigger offers
|
77
|
+
# fe # -> lists all available resource types
|
78
|
+
def self.help_values
|
79
|
+
end
|
80
|
+
|
81
|
+
# ---------------------------------------------------------------------
|
82
|
+
# JS
|
83
|
+
|
84
|
+
# trigger dropdown when user input matches this regexp
|
85
|
+
def self.match_regexp
|
86
|
+
raise NotImplementedError
|
87
|
+
end
|
88
|
+
|
89
|
+
# index of the match group from the above regexp to be used for matching
|
90
|
+
def self.match_index
|
91
|
+
raise NotImplementedError
|
92
|
+
end
|
93
|
+
|
94
|
+
# tag that gets inserted into the field
|
95
|
+
def self.replace
|
96
|
+
raise NotImplementedError
|
97
|
+
end
|
98
|
+
|
99
|
+
# fragment to be displayed in the dropdown menu
|
100
|
+
def self.template
|
101
|
+
raise NotImplementedError
|
102
|
+
end
|
103
|
+
|
104
|
+
# ---------------------------------------------------------------------
|
105
|
+
|
106
|
+
# return JSON version of resources that match query
|
107
|
+
# this is returned when user starts typing (the query)
|
108
|
+
def self.json_for_query(query)
|
109
|
+
raise NotImplementedError
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.values(hashtag_classes = Variable.descendants)
|
113
|
+
end
|
114
|
+
|
115
|
+
# ---------------------------------------------------------------------
|
116
|
+
|
117
|
+
# converts Ruby tegexp to JS regexp
|
118
|
+
def self.json_regexp(regexp)
|
119
|
+
str = regexp.inspect
|
120
|
+
.sub('\\A', '^')
|
121
|
+
.sub('\\Z', '$')
|
122
|
+
.sub('\\z', '$')
|
123
|
+
.sub(/^\//, '')
|
124
|
+
.sub(/\/[a-z]*$/, '')
|
125
|
+
.gsub(/\(\?#.+\)/, '')
|
126
|
+
.gsub(/\(\?-\w+:/, '(')
|
127
|
+
.gsub(/\s/, '')
|
128
|
+
Regexp.new(str).source
|
129
|
+
end
|
130
|
+
|
131
|
+
# ---------------------------------------------------------------------
|
132
|
+
|
133
|
+
# Converts hash tags to markup
|
134
|
+
def to_markup
|
135
|
+
str.to_s.gsub(self.class.regexp) do |match|
|
136
|
+
m = markup(Regexp.last_match)
|
137
|
+
match = m || match
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Updates hash tags
|
142
|
+
def to_hashtag
|
143
|
+
str.to_s.gsub(self.class.regexp) do |match|
|
144
|
+
ht = hashtag(Regexp.last_match)
|
145
|
+
match = ht || match
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# ---------------------------------------------------------------------
|
150
|
+
|
151
|
+
# the proper hashtag (so it can be updated automatically)
|
152
|
+
def hashtag(match)
|
153
|
+
raise NotImplementedError
|
154
|
+
end
|
155
|
+
|
156
|
+
# what is the hashtag replaced with in the end
|
157
|
+
def markup(match)
|
158
|
+
raise NotImplementedError
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Hashtags
|
2
|
+
class Builder < Struct.new(:options)
|
3
|
+
def self.to_markup(str, options = {})
|
4
|
+
new(options).to_markup(str)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.to_hashtag(str, options = {})
|
8
|
+
new(options).to_hashtag(str)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.dom_data(options = {})
|
12
|
+
new(options).dom_data
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.help(options = {})
|
16
|
+
new(options).help
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
super(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# collects markup from all hashtags classes
|
24
|
+
def to_markup(str)
|
25
|
+
hashtag_classes.inject(str) { |res, cls| cls.to_markup(res) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# collects hashtags from all hashtags classes
|
29
|
+
def to_hashtag(str)
|
30
|
+
hashtag_classes.inject(str) { |res, cls| cls.to_hashtag(res) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# render textcomplete dom data
|
34
|
+
def dom_data
|
35
|
+
{ hashtags: {
|
36
|
+
path: Engine.routes.url_helpers.hashtags_resources_path,
|
37
|
+
strategies: hashtag_strategies
|
38
|
+
} }
|
39
|
+
end
|
40
|
+
|
41
|
+
# render help string
|
42
|
+
def help
|
43
|
+
hashtag_classes.group_by(&:trigger).map do |trigger, cls|
|
44
|
+
OpenStruct.new(hashtag_classes: cls, trigger: trigger, help_values: cls.map(&:help_values).flatten.compact.sort)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def filter_classes(cls)
|
49
|
+
return [] unless cls.present?
|
50
|
+
res = cls
|
51
|
+
res &= options[:only] if options[:only]
|
52
|
+
res -= options[:except] if options[:except]
|
53
|
+
res
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def hashtag_classes
|
59
|
+
filter_classes(
|
60
|
+
Resource.resource_classes +
|
61
|
+
User.user_classes +
|
62
|
+
Variable.variable_classes
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def hashtag_strategies
|
67
|
+
cls = hashtag_classes.dup
|
68
|
+
|
69
|
+
# add resource type strategy if needed
|
70
|
+
cls << ResourceType if cls.any? { |c| c < Resource } && !cls.include?(ResourceType)
|
71
|
+
|
72
|
+
# remove all variable classes and replace them with one strategy
|
73
|
+
if variable_cls = cls.select { |c| c < Variable } && variable_cls.present?
|
74
|
+
cls -= variable_cls
|
75
|
+
cls << Variable
|
76
|
+
end
|
77
|
+
|
78
|
+
cls.collect { |c| c.strategy(hashtag_classes) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'mongoid'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
Mongoid::Fields.option :hashtags do |cls, field, value|
|
5
|
+
return if value == false
|
6
|
+
|
7
|
+
cls.define_singleton_method(:hashtags) { @hashtags ||= {} } unless cls.respond_to?(:hashtags)
|
8
|
+
options = value.is_a?(Hash) ? value.slice(*%i(only except)) : {}
|
9
|
+
|
10
|
+
cls.hashtags[field.name].define_singleton_method :dom_data do
|
11
|
+
Hashtags::Builder.dom_data(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
cls.hashtags[field.name].define_singleton_method :help do
|
15
|
+
Hashtags::Builder.help(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
cls.hashtags[field.name].define_singleton_method :options do
|
19
|
+
options
|
20
|
+
end
|
21
|
+
|
22
|
+
field.define_singleton_method :demongoize do |*args|
|
23
|
+
res = super(*args)
|
24
|
+
res.define_singleton_method :to_markup do
|
25
|
+
Hashtags::Builder.to_markup(res.to_s, options).html_safe
|
26
|
+
end
|
27
|
+
res.define_singleton_method :to_hashtag do
|
28
|
+
Hashtags::Builder.to_hashtag(res.to_s, options).html_safe
|
29
|
+
end
|
30
|
+
res
|
31
|
+
end
|
32
|
+
|
33
|
+
field.define_singleton_method :mongoize do |value|
|
34
|
+
Hashtags::Builder.to_hashtag(super(value.to_s), options)
|
35
|
+
end
|
36
|
+
end
|