hashtags 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|