highlight-within-textarea-rails 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.
@@ -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: []