highlight-within-textarea-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fca4600b2958b37492eefd322d3eccbbaef6d568
4
+ data.tar.gz: 769b20b03f15f7df573be62a25dedcdacdd9d99f
5
+ SHA512:
6
+ metadata.gz: '01228d403dfbb99b0e471e2f6905a8f6953022b7c0c5368e0bed5069053396eb06a171568940e82e550b15a5e320eff1543063054f45d1624344f84ad1ebc47b'
7
+ data.tar.gz: f14ba7338a1c2ad920185232736decc7d9ab02f73966d56f13ceafb9928efb03c5418a79048b12c8b6a3d8567c018cec5c0aa3b3da8b27b37b5e6df68af4e744
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in highlight-within-textarea-rails.gemspec
6
+ gemspec
@@ -0,0 +1,47 @@
1
+ # Highlight::Within::Textarea::Rails
2
+
3
+ ## What is this?
4
+
5
+ A gemified version of **highlight-within-textarea** (jQuery plugin for highlighting bits of text within a textarea).
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'highlight-within-textarea-rails'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install highlight-within-textarea-rails
22
+
23
+ ## Usage
24
+
25
+ Add the following line to `app/assets/javascripts/application.js`:
26
+
27
+ ```javascript
28
+ //= require jquery.highlight-within-textarea.js
29
+ ```
30
+
31
+ and `app/assets/stylesheets/application.css`:
32
+
33
+ ```css
34
+ *= require jquery.highlight-within-textarea.css
35
+ ```
36
+
37
+ and then [RTFM](https://github.com/lonekorean/highlight-within-textarea).
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
42
+
43
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ukazap/highlight-within-textarea-rails.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "highlight/within/textarea/rails"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "highlight/within/textarea/rails/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "highlight-within-textarea-rails"
8
+ spec.version = Highlight::Within::Textarea::Rails::VERSION
9
+ spec.authors = ["Ukaza Perdana"]
10
+ spec.email = ["hello@ukazap.space"]
11
+
12
+ spec.summary = "Gemified version of highlight-within-textarea"
13
+ spec.description = "jQuery plugin for highlighting bits of text within a textarea."
14
+ spec.homepage = "https://github.com/ukazap/highlight-within-textarea-rails"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.15"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ end
@@ -0,0 +1,12 @@
1
+ require "highlight/within/textarea/rails/version"
2
+
3
+ module Highlight
4
+ module Within
5
+ module Textarea
6
+ module Rails
7
+ class Engine < ::Rails::Engine
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module Highlight
2
+ module Within
3
+ module Textarea
4
+ module Rails
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,380 @@
1
+ /*
2
+ * highlight-within-textarea
3
+ *
4
+ * @author Will Boyd
5
+ * @github https://github.com/lonekorean/highlight-within-textarea
6
+ */
7
+
8
+ (function($) {
9
+ let ID = 'hwt';
10
+
11
+ let HighlightWithinTextarea = function($el, config) {
12
+ this.init($el, config);
13
+ };
14
+
15
+ HighlightWithinTextarea.prototype = {
16
+ init: function($el, config) {
17
+ this.$el = $el;
18
+
19
+ // backwards compatibility with v1 (deprecated)
20
+ if (this.getType(config) === 'function') {
21
+ config = { highlight: config };
22
+ }
23
+
24
+ if (this.getType(config) === 'custom') {
25
+ this.highlight = config;
26
+ this.generate();
27
+ } else {
28
+ console.error('valid config object not provided');
29
+ }
30
+ },
31
+
32
+ // returns identifier strings that aren't necessarily "real" JavaScript types
33
+ getType: function(instance) {
34
+ let type = typeof instance;
35
+ if (!instance) {
36
+ return 'falsey';
37
+ } else if (Array.isArray(instance)) {
38
+ if (instance.length === 2 && typeof instance[0] === 'number' && typeof instance[1] === 'number') {
39
+ return 'range';
40
+ } else {
41
+ return 'array';
42
+ }
43
+ } else if (type === 'object') {
44
+ if (instance instanceof RegExp) {
45
+ return 'regexp';
46
+ } else if (instance.hasOwnProperty('highlight')) {
47
+ return 'custom';
48
+ }
49
+ } else if (type === 'function' || type === 'string') {
50
+ return type;
51
+ }
52
+
53
+ return 'other';
54
+ },
55
+
56
+ generate: function() {
57
+ this.$el
58
+ .addClass(ID + '-input ' + ID + '-content')
59
+ .on('input.' + ID, this.handleInput.bind(this))
60
+ .on('scroll.' + ID, this.handleScroll.bind(this));
61
+
62
+ this.$highlights = $('<div>', { class: ID + '-highlights ' + ID + '-content' });
63
+
64
+ this.$backdrop = $('<div>', { class: ID + '-backdrop' })
65
+ .append(this.$highlights);
66
+
67
+ this.$container = $('<div>', { class: ID + '-container' })
68
+ .insertAfter(this.$el)
69
+ .append(this.$backdrop, this.$el) // moves $el into $container
70
+ .on('scroll', this.blockContainerScroll.bind(this));
71
+
72
+ this.browser = this.detectBrowser();
73
+ switch (this.browser) {
74
+ case 'firefox':
75
+ this.fixFirefox();
76
+ break;
77
+ case 'ios':
78
+ this.fixIOS();
79
+ break;
80
+ }
81
+
82
+ // plugin function checks this for success
83
+ this.isGenerated = true;
84
+
85
+ // trigger input event to highlight any existing input
86
+ this.handleInput();
87
+ },
88
+
89
+ // browser sniffing sucks, but there are browser-specific quirks to handle
90
+ // that are not a matter of feature detection
91
+ detectBrowser: function() {
92
+ let ua = window.navigator.userAgent.toLowerCase();
93
+ if (ua.indexOf('firefox') !== -1) {
94
+ return 'firefox';
95
+ } else if (!!ua.match(/msie|trident\/7|edge/)) {
96
+ return 'ie';
97
+ } else if (!!ua.match(/ipad|iphone|ipod/) && ua.indexOf('windows phone') === -1) {
98
+ // Windows Phone flags itself as "like iPhone", thus the extra check
99
+ return 'ios';
100
+ } else {
101
+ return 'other';
102
+ }
103
+ },
104
+
105
+ // Firefox doesn't show text that scrolls into the padding of a textarea, so
106
+ // rearrange a couple box models to make highlights behave the same way
107
+ fixFirefox: function() {
108
+ // take padding and border pixels from highlights div
109
+ let padding = this.$highlights.css([
110
+ 'padding-top', 'padding-right', 'padding-bottom', 'padding-left'
111
+ ]);
112
+ let border = this.$highlights.css([
113
+ 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'
114
+ ]);
115
+ this.$highlights.css({
116
+ 'padding': '0',
117
+ 'border-width': '0'
118
+ });
119
+
120
+ this.$backdrop
121
+ .css({
122
+ // give padding pixels to backdrop div
123
+ 'margin-top': '+=' + padding['padding-top'],
124
+ 'margin-right': '+=' + padding['padding-right'],
125
+ 'margin-bottom': '+=' + padding['padding-bottom'],
126
+ 'margin-left': '+=' + padding['padding-left'],
127
+ })
128
+ .css({
129
+ // give border pixels to backdrop div
130
+ 'margin-top': '+=' + border['border-top-width'],
131
+ 'margin-right': '+=' + border['border-right-width'],
132
+ 'margin-bottom': '+=' + border['border-bottom-width'],
133
+ 'margin-left': '+=' + border['border-left-width'],
134
+ });
135
+ },
136
+
137
+ // iOS adds 3px of (unremovable) padding to the left and right of a textarea,
138
+ // so adjust highlights div to match
139
+ fixIOS: function() {
140
+ this.$highlights.css({
141
+ 'padding-left': '+=3px',
142
+ 'padding-right': '+=3px'
143
+ });
144
+ },
145
+
146
+ handleInput: function() {
147
+ let input = this.$el.val();
148
+ let ranges = this.getRanges(input, this.highlight);
149
+ let unstaggeredRanges = this.removeStaggeredRanges(ranges);
150
+ let boundaries = this.getBoundaries(unstaggeredRanges);
151
+ this.renderMarks(boundaries);
152
+ },
153
+
154
+ getRanges: function(input, highlight) {
155
+ let type = this.getType(highlight);
156
+ switch (type) {
157
+ case 'array':
158
+ return this.getArrayRanges(input, highlight);
159
+ case 'function':
160
+ return this.getFunctionRanges(input, highlight);
161
+ case 'regexp':
162
+ return this.getRegExpRanges(input, highlight);
163
+ case 'string':
164
+ return this.getStringRanges(input, highlight);
165
+ case 'range':
166
+ return this.getRangeRanges(input, highlight);
167
+ case 'custom':
168
+ return this.getCustomRanges(input, highlight);
169
+ default:
170
+ if (!highlight) {
171
+ // do nothing for falsey values
172
+ return [];
173
+ } else {
174
+ console.error('unrecognized highlight type');
175
+ }
176
+ }
177
+ },
178
+
179
+ getArrayRanges: function(input, arr) {
180
+ let ranges = arr.map(this.getRanges.bind(this, input));
181
+ return Array.prototype.concat.apply([], ranges);
182
+ },
183
+
184
+ getFunctionRanges: function(input, func) {
185
+ return this.getRanges(input, func(input));
186
+ },
187
+
188
+ getRegExpRanges: function(input, regex) {
189
+ let ranges = [];
190
+ let match;
191
+ while (match = regex.exec(input), match !== null) {
192
+ ranges.push([match.index, match.index + match[0].length]);
193
+ if (!regex.global) {
194
+ // non-global regexes do not increase lastIndex, causing an infinite loop,
195
+ // but we can just break manually after the first match
196
+ break;
197
+ }
198
+ }
199
+ return ranges;
200
+ },
201
+
202
+ getStringRanges: function(input, str) {
203
+ let ranges = [];
204
+ let inputLower = input.toLowerCase();
205
+ let strLower = str.toLowerCase();
206
+ let index = 0;
207
+ while (index = inputLower.indexOf(strLower, index), index !== -1) {
208
+ ranges.push([index, index + strLower.length]);
209
+ index += strLower.length;
210
+ }
211
+ return ranges;
212
+ },
213
+
214
+ getRangeRanges: function(input, range) {
215
+ return [range];
216
+ },
217
+
218
+ getCustomRanges: function(input, custom) {
219
+ let ranges = this.getRanges(input, custom.highlight);
220
+ if (custom.className) {
221
+ ranges.forEach(function(range) {
222
+ // persist class name as a property of the array
223
+ if (range.className) {
224
+ range.className = custom.className + ' ' + range.className;
225
+ } else {
226
+ range.className = custom.className;
227
+ }
228
+ });
229
+ }
230
+ return ranges;
231
+ },
232
+
233
+ // prevent staggered overlaps (clean nesting is fine)
234
+ removeStaggeredRanges: function(ranges) {
235
+ let unstaggeredRanges = [];
236
+ ranges.forEach(function(range) {
237
+ let isStaggered = unstaggeredRanges.some(function(unstaggeredRange) {
238
+ let isStartInside = range[0] > unstaggeredRange[0] && range[0] < unstaggeredRange[1];
239
+ let isStopInside = range[1] > unstaggeredRange[0] && range[1] < unstaggeredRange[1];
240
+ return isStartInside !== isStopInside; // xor
241
+ });
242
+ if (!isStaggered) {
243
+ unstaggeredRanges.push(range);
244
+ }
245
+ });
246
+ return unstaggeredRanges;
247
+ },
248
+
249
+ getBoundaries: function(ranges) {
250
+ let boundaries = [];
251
+ ranges.forEach(function(range) {
252
+ boundaries.push({
253
+ type: 'start',
254
+ index: range[0],
255
+ className: range.className
256
+ });
257
+ boundaries.push({
258
+ type: 'stop',
259
+ index: range[1]
260
+ });
261
+ });
262
+
263
+ this.sortBoundaries(boundaries);
264
+ return boundaries;
265
+ },
266
+
267
+ sortBoundaries: function(boundaries) {
268
+ // backwards sort (since marks are inserted right to left)
269
+ boundaries.sort(function(a, b) {
270
+ if (a.index !== b.index) {
271
+ return b.index - a.index;
272
+ } else if (a.type === 'stop' && b.type === 'start') {
273
+ return 1;
274
+ } else if (a.type === 'start' && b.type === 'stop') {
275
+ return -1;
276
+ } else {
277
+ return 0;
278
+ }
279
+ });
280
+ },
281
+
282
+ renderMarks: function(boundaries) {
283
+ let input = this.$el.val();
284
+ boundaries.forEach(function(boundary, index) {
285
+ let markup;
286
+ if (boundary.type === 'start') {
287
+ markup = '{{hwt-mark-start|' + index + '}}';
288
+ } else {
289
+ markup = '{{hwt-mark-stop}}';
290
+ }
291
+ input = input.slice(0, boundary.index) + markup + input.slice(boundary.index);
292
+ });
293
+
294
+ // this keeps scrolling aligned when input ends with a newline
295
+ input = input.replace(/\n(\{\{hwt-mark-stop\}\})?$/, '\n\n$1');
296
+
297
+ // encode HTML entities
298
+ input = input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
299
+
300
+ if (this.browser === 'ie') {
301
+ // IE/Edge wraps whitespace differently in a div vs textarea, this fixes it
302
+ input = input.replace(/ /g, ' <wbr>');
303
+ }
304
+
305
+ // replace start tokens with opening <mark> tags with class name
306
+ input = input.replace(/\{\{hwt-mark-start\|(\d+)\}\}/g, function(match, submatch) {
307
+ var className = boundaries[+submatch].className;
308
+ if (className) {
309
+ return '<mark class="' + className + '">';
310
+ } else {
311
+ return '<mark>';
312
+ }
313
+ });
314
+
315
+ // replace stop tokens with closing </mark> tags
316
+ input = input.replace(/\{\{hwt-mark-stop\}\}/g, '</mark>');
317
+
318
+ this.$highlights.html(input);
319
+ },
320
+
321
+ handleScroll: function() {
322
+ let scrollTop = this.$el.scrollTop();
323
+ this.$backdrop.scrollTop(scrollTop);
324
+
325
+ // Chrome and Safari won't break long strings of spaces, which can cause
326
+ // horizontal scrolling, this compensates by shifting highlights by the
327
+ // horizontally scrolled amount to keep things aligned
328
+ let scrollLeft = this.$el.scrollLeft();
329
+ this.$backdrop.css('transform', (scrollLeft > 0) ? 'translateX(' + -scrollLeft + 'px)' : '');
330
+ },
331
+
332
+ // in Chrome, page up/down in the textarea will shift stuff within the
333
+ // container (despite the CSS), this immediately reverts the shift
334
+ blockContainerScroll: function() {
335
+ this.$container.scrollLeft(0);
336
+ },
337
+
338
+ destroy: function() {
339
+ this.$backdrop.remove();
340
+ this.$el
341
+ .unwrap()
342
+ .removeClass(ID + '-text ' + ID + '-input')
343
+ .off(ID)
344
+ .removeData(ID);
345
+ },
346
+ };
347
+
348
+ // register the jQuery plugin
349
+ $.fn.highlightWithinTextarea = function(options) {
350
+ return this.each(function() {
351
+ let $this = $(this);
352
+ let plugin = $this.data(ID);
353
+
354
+ if (typeof options === 'string') {
355
+ if (plugin) {
356
+ switch (options) {
357
+ case 'update':
358
+ plugin.handleInput();
359
+ break;
360
+ case 'destroy':
361
+ plugin.destroy();
362
+ break;
363
+ default:
364
+ console.error('unrecognized method string');
365
+ }
366
+ } else {
367
+ console.error('plugin must be instantiated first');
368
+ }
369
+ } else {
370
+ if (plugin) {
371
+ plugin.destroy();
372
+ }
373
+ plugin = new HighlightWithinTextarea($this, options);
374
+ if (plugin.isGenerated) {
375
+ $this.data(ID, plugin);
376
+ }
377
+ }
378
+ });
379
+ };
380
+ })(jQuery);
@@ -0,0 +1,48 @@
1
+ .hwt-container {
2
+ display: inline-block;
3
+ position: relative;
4
+ overflow: hidden !important;
5
+ -webkit-text-size-adjust: none !important;
6
+ }
7
+
8
+ .hwt-backdrop {
9
+ position: absolute !important;
10
+ top: 0 !important;
11
+ right: -99px !important;
12
+ bottom: 0 !important;
13
+ left: 0 !important;
14
+ padding-right: 99px !important;
15
+ overflow-x: hidden !important;
16
+ overflow-y: auto !important;
17
+ }
18
+
19
+ .hwt-highlights {
20
+ width: auto !important;
21
+ height: auto !important;
22
+ border-color: transparent !important;
23
+ white-space: pre-wrap !important;
24
+ word-wrap: break-word !important;
25
+ color: transparent !important;
26
+ overflow: hidden !important;
27
+ }
28
+
29
+ .hwt-input {
30
+ display: block !important;
31
+ position: relative !important;
32
+ margin: 0;
33
+ padding: 0;
34
+ border-radius: 0;
35
+ font: inherit;
36
+ overflow-x: hidden !important;
37
+ overflow-y: auto !important;
38
+ }
39
+
40
+ .hwt-content {
41
+ border: 1px solid;
42
+ background: none transparent !important;
43
+ }
44
+
45
+ .hwt-content mark {
46
+ padding: 0 !important;
47
+ color: inherit;
48
+ }
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: highlight-within-textarea-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ukaza Perdana
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: jQuery plugin for highlighting bits of text within a textarea.
42
+ email:
43
+ - hello@ukazap.space
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - Gemfile
50
+ - README.md
51
+ - Rakefile
52
+ - bin/console
53
+ - bin/setup
54
+ - highlight-within-textarea-rails.gemspec
55
+ - lib/highlight/within/textarea/rails.rb
56
+ - lib/highlight/within/textarea/rails/version.rb
57
+ - vendor/assets/javascripts/jquery.highlight-within-textarea.js
58
+ - vendor/assets/stylesheets/jquery.highlight-within-textarea.css
59
+ homepage: https://github.com/ukazap/highlight-within-textarea-rails
60
+ licenses: []
61
+ metadata: {}
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project:
78
+ rubygems_version: 2.6.11
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: Gemified version of highlight-within-textarea
82
+ test_files: []