// Rizzoma Extended by Yura Babak
// https://github.com/Inversion-des/Rizzoma-Extended
// ==UserScript==
// @name Rizzoma Extended
// @description Розширення додає поле для пошуку по сторінці на сервісі rizzoma.com
// @author Yura Babak
// @namespace Rizzoma
// @version 0.9.4
// @include https://rizzoma.com/*
// @run-at document-body
// @supportURL https://github.com/Inversion-des/Rizzoma-Extended/issues
// ==/UserScript==
"use strict";
!function(win) {
if (window != window.top) return
var doc = win.document
var $ = win.jQuery
var $win = $(win)
// data
var data = $.extend(true, {}, win.getWaveWithBlipsResults)
var block_by_id_H = {}
var index_blocks = function() {
// get first wave data
for (var k in data) {
var wave = data[k]
break
}
// кожен blip — це є окремий незалежний блок, який може редагуватися
// спочатку проходимося, робимо базовий індекс
$.each(wave.data.blips, function(i, blip) {
//. skip root container
if (blip.snapshot.isContainer) return true
var block = {
id: blip.docId,
children_ids: [],
parents: []
}
block_by_id_H[block.id] = block
})
// тепер аналізуємо контент кожного блока
$.each(wave.data.blips, function(i, blip) {
//. skip root container
if (blip.snapshot.isContainer) return true
var block = block_by_id_H[blip.docId]
var text_parts = []
$.each(blip.snapshot.content, function(i, part) {
if (part.params.__TYPE == "TEXT") {
text_parts.push(part.t)
}
else if (part.params.__TYPE == "BLIP") {
text_parts.push('(+)')
var id = part.params.__ID
block.children_ids.push(id)
var child_block = block_by_id_H[id]
if (child_block) {
child_block.thread_id = part.params.__THREAD_ID
child_block.parents = block.parents.concat(block)
}
}
})
block.text = text_parts.join("\n").toLocaleLowerCase()
block_by_id_H[block.id] = block
})
// detect big document
tree.f_doc_is_big = Object.keys(block_by_id_H).length > 1000
}
setTimeout(index_blocks, 500)
/////// ------- tree ------- ///////////////////////////////////////////////////////////////////////////////
var tree = {}
tree.nodes_with_hl = []
tree.blips_with_matched = []
tree.all_unfolded = []
// tree.fold_all({in:cont})
tree.fold_all = function(o) {
o = o || {}
var cont = o.in || $('.root-thread')
// if folding all, not in container — clear unfolded markers
if (!o.in) {
$.each(tree.all_unfolded, function(i, plus) {
delete plus._f_unfolded
})
tree.all_unfolded = []
}
cont.find('span.blip-thread:not(.folded)').each(function() {
var plus = this.rzBlipThread
// skip (+) els wich were unfolded during unfold_all_to_target
if (!plus._f_unfolded) plus.fold()
})
}
// tree.unfold_all_to_target({id:'0_b_9bl0_83je6'})
tree.unfold_all_to_target = function(o) {
var id = o.id
var next_block, res={}
var target_block = block_by_id_H[id]
var cur_container = $('.root-blip').parent()
// process all the parents + target_block
$.each(target_block.parents, function(i, block) {
next_block = target_block.parents[i+1] || target_block
// find and unfold proper (+) in the current container
cur_container.find('span.blip-thread').each(function() {
var plus = this.rzBlipThread
var blips_cont = $(this).closest('.blips-container')
// skip if it is not a child
if (!blips_cont.is(cur_container[0])) return true
if (plus._threadId == next_block.thread_id) {
plus.unfold()
// mark unfolded
plus._f_unfolded = true
tree.all_unfolded.push(plus)
// mark parent container
var blip = $(this).closest('.blip-container')
blip[0]._f_has_matched = true
blip.removeClass('RExt_blip_shaded')
tree.blips_with_matched.push(blip)
// shade all sibling blips
cur_container.children('.blip-container').each(function(i, blip) {
if (!blip._f_has_matched) {
blip = $(blip)
blip.addClass('RExt_blip_shaded')
}
})
//. change current container
cur_container = $(plus._blipsContainer)
tree.fold_all({in:cur_container})
// break
return false
}
})
})
return {last_container:cur_container}
}
// tree.search_text({text:'__'})
tree.search_text = function(o) {
var text = o.text.toLocaleLowerCase()
var res
var rx = new RegExp('('+RegExp.escape(text)+')', 'ig')
tree.fold_all()
tree.clear_hl()
var results_count = 0
// for each block
$.each(block_by_id_H, function(id, block) {
if (block.text && block.text.indexOf(text)>-1) {
res = tree.unfold_all_to_target({id:block.id})
// process all the blips in container
res.last_container.children('.blip-container').each(function(i, blip) {
blip = $(blip)
// (<!) process only target blip
if (blip[0].__rizzoma_data_key.params.__ID != block.id) {
if (!blip[0]._f_has_matched) blip.addClass('RExt_blip_shaded')
return true
}
blip[0]._f_has_matched = true
blip.removeClass('RExt_blip_shaded')
tree.blips_with_matched.push(blip)
var children = blip.find('> .js-editor-container > .js-editor').children('div, li').children(':not(.blip-thread)')
children.each(function(i, node) {
if (node._fv_ori_text) return true
node = $(node)
var node_text = node.html()
if (node_text.toLocaleLowerCase().indexOf(text)<0) return true
node[0]._fv_ori_text = node_text
var node_text_with_hl = node_text.replace(rx, '<b class="RExt_text_hl">$1</b>')
node.html(node_text_with_hl)
// this data for text nodes used in api
node.hl_el = node.find('b')
node.hl_el[0].data = text
tree.nodes_with_hl.push(node)
})
})
}
})
}
tree.clear_node_hl = function(node) {
node.html(node[0]._fv_ori_text)
delete node[0]._fv_ori_text
}
tree.clear_hl = function(o) {
$.each(tree.nodes_with_hl, function(i, node) {
tree.clear_node_hl(node)
})
tree.nodes_with_hl = []
$.each(tree.blips_with_matched, function(i, blip) {
delete blip[0]._f_has_matched
})
tree.blips_with_matched = []
$('.root-thread .RExt_blip_shaded').removeClass('RExt_blip_shaded')
$win.trigger('tree.clear_hl')
}
/////// ------- /tree ------- ///////////////////////////////////////////////////////////////////////////////
// export
win.RExt_tree = tree
win.RExt_block_by_id_H = block_by_id_H
// -styles
$('<style>\
.RExt_search_cont {display: inline-block; position: absolute; top: 5px; right: 50px;}\
.RExt_search_cont input {width:300px;background-color:#F6F6F6;padding:10px;padding-right:60px; border-radius: 5px; border: 1px solid #859099;}\
.RExt_search_cont input:focus {background-color:#FFF;}\
.RExt_search_cont input:focus::placeholder {opacity:0.2}\
.RExt_search_cont input:focus::-moz-placeholder {opacity:0.2}\
.RExt_search_cont input:focus::-webkit-input-placeholder {opacity:0.2}\
.RExt_search_cont input:focus:-ms-input-placeholder {opacity:0.2}\
.RExt_search_cont__x {display:none;position:absolute;top:0;right:0;padding:10px;cursor:pointer;font-size: 24px;line-height: 19px;}\
.RExt_search_cont__x:hover {color:#BD2929;}\
.RExt_search_cont__search_btn {display:none;position:absolute;top:1px;right:1px;background-color:#EFEBAB;padding:10px;cursor:pointer;font-size:12px;line-height:14px;border-radius: 0 5px 5px 0;border-left:1px solid #e6dbae;}\
.RExt_search_cont__search_btn:hover {background-color:#F5EE8B;}\
.RExt_text_hl {font-weight:normal;background-color:#FFFF00;outline:10px solid transparent;outline-offset:10px;}\
.RExt_text_hl._hl_first {border-top:2px solid transparent;}\
.RExt_text_hl._hl_last {border-bottom:2px solid transparent;}\
.RExt_text_hl._hl_focused {outline: 5px solid rgba(255, 224, 102, 0.6);outline-offset:0px;transition: all 0.3s ease;}\
.RExt_text_hl._hl_focused._hl_first {border-top:2px solid #F27316;}\
.RExt_text_hl._hl_focused._hl_last {border-bottom:2px solid #F27316;}\
.RExt_search_cont._showing_results input {background-color:#FFFFC7;}\
.RExt_search_cont__results {display:none;position:absolute;left:-56px;width:50px;text-align:right;top:7px;color:#FFF;}\
.RExt_search_cont__results div {display:inline;}\
.RExt_blip_shaded {padding:0px;height:15px;opacity:0.4;min-height:0px;overflow:hidden;cursor:pointer;}\
.RExt_blip_shaded * {cursor:pointer;}\
</style>').appendTo('head')
// DOM ready
$(function() {
// wait for header
var header
var check_dom = function() {
header = $('.js-wave-header')
header[0] ? $win.trigger('RExt_head_ready') : setTimeout(check_dom, 100)
}
check_dom()
// on header ready
$win.on('RExt_head_ready', function() {
var scroll_cont = $('.js-wave-blips')
// search
var search = {}
search.cont = $('\
<div class="RExt_search_cont">\
<div class="RExt_search_cont__results">\
<div class="RExt_search_cont__results__cur_index"></div>\
<div class="RExt_search_cont__results__count"></div>\
</div>\
<input type="text" value="" placeholder="Пошук по сторінці" title="Hotkey: /" />\
<div class="RExt_search_cont__search_btn" title="Hotkey: Enter">Пошук</div>\
<div class="RExt_search_cont__x" title="Hotkey: Esc">×</div>\
</div>\
').hide().insertBefore(header.find('.js-settings-container')).delay(1000).fadeIn('slow')
// do search
search.last_search_text = null
search.do_search = function() {
var val = search.input.clear_val()
// (<!) ignore same text
if (!val || val == search.last_search_text) return;
search.last_search_text = val
search.search_btn.hide()
tree.search_text({text:val})
search.cont.addClass('_showing_results')
search.x.show()
// results
search.results.count.text(tree.nodes_with_hl.length)
search.results.cur_index.text('')
search.results.stop().fadeIn()
// sort nodes by y position on the page
tree.nodes_with_hl.sort(function(a, b) {
a.c_top = a.c_top || a.offset().top
b.c_top = b.c_top || b.offset().top
return a.c_top - b.c_top
})
// mark first and last
if (tree.nodes_with_hl[0]) {
tree.nodes_with_hl[0].hl_el.addClass('_hl_first')
tree.nodes_with_hl[tree.nodes_with_hl.length-1].hl_el.addClass('_hl_last')
}
// go to the first result
search.cur_result_index = 0
search.go_to_cur_result()
}
// go to
search.cur_result_index = 0
search.last_focused_hl = $()
search.go_to_cur_result = function() {
if (!tree.nodes_with_hl.length) return;
// index correction
if (search.cur_result_index < 0) search.cur_result_index = 0
if (search.cur_result_index > tree.nodes_with_hl.length-1) search.cur_result_index = tree.nodes_with_hl.length-1
// show index in results (only not 1)
search.results.cur_index.text( (search.cur_result_index || search.results.cur_index.text()) ? search.cur_result_index+1+' /' : '')
var hl_node = tree.nodes_with_hl[search.cur_result_index]
var pos_top = hl_node.offset().top
var delta_y = pos_top - (scroll_cont.height()/2) + 50
var action = function() {
search.last_focused_hl.removeClass('_hl_focused')
hl_node.hl_el.addClass('_hl_focused')
search.last_focused_hl = hl_node.hl_el
}
var f_at_the_edge = delta_y <0 && scroll_cont.scrollTop() == 0 || delta_y >0 && scroll_cont.scrollTop() == scroll_cont.getScrollTopMax()
if (f_at_the_edge) {
action()
}
else {
scroll_cont.stop(true, true).animate({scrollTop: '+='+delta_y}, Math.abs(delta_y), action)
}
}
//-- input
search.input = search.cont.find('input')
// on type
var t_delayed_search = null
search.input.on('input', function() {
var val = search.input.val()
search.last_search_text = false
tree.clear_hl()
search.x.hide()
search.search_btn.toggle(!!val)
// (<!) do not auto-search for big docs
if (tree.f_doc_is_big) return;
var val = search.input.clear_val()
clearTimeout(t_delayed_search)
if (val.length>3) {
t_delayed_search = setTimeout(function() {
search.do_search()
}, 500)
}
})
search.input.clear_val = function() {
return $.trim(this.val())
}
// input action keys
search.input.on('keydown', function(e) {
switch (e.which) {
case $.key.Enter:
search.do_search()
break
case $.key.Esc:
search.x.click()
break
case $.key.Down:
case $.key.Up:
search.cur_result_index += ( e.which == $.key.Down ? 1 : -1 )
search.go_to_cur_result()
break
}
})
// Search btn (Enter)
search.search_btn = search.cont.find('.RExt_search_cont__search_btn')
search.search_btn.on('mousedown', function() {
search.do_search()
})
// X — clear search
search.x = search.cont.find('.RExt_search_cont__x')
search.x.on('click', function() {
tree.clear_hl()
search.input.val('').focus().triggerHandler('input')
})
// results
search.results = search.cont.find('.RExt_search_cont__results')
search.results.cur_index = search.results.find('.RExt_search_cont__results__cur_index')
search.results.count = search.results.find('.RExt_search_cont__results__count')
// -hot keys (-hotkeys)
$win.on('keydown.RExt', function(e) {
switch (e.which) {
case $.key.Slash:
case $.key.Slash_cyr:
if ($(doc.activeElement).is('input')) return true
e.preventDefault()
search.input.focus()
break
}
})
// for shared blips
// on click — unshade
$('.js-wave-content').on('mousedown', '.RExt_blip_shaded', function(e) {
var blip = $(this)
blip.removeClass('RExt_blip_shaded')
return false
})
//-- on edit blip — clear any hl in nodes (-edit, -clear)
var cur_blip = $()
// change cur_blip on click inside every blip
$('.js-wave-content').on('mousedown', '.blip-container', function(e) {
// *e will bubble up, but cur_blip will be changed only on target blip
if (!e._f_blip_saved) cur_blip = $(this)
e._f_blip_saved = true
})
// returns all hl nodes inside some node
var find_hl_nodes = function(in_node) {
var res = $()
in_node.children().each(function() {
var node = $(this)
if (node.is('.RExt_text_hl')) {
res = res.add(node)
}
// *we should skip all the nested blips here, otherwise when you edit some root blip — all the highlighted search results in nested blips will be removed
else if (node.is(':not(.blip-thread)')) {
// recursively search in child node
res = res.add( find_hl_nodes(node) )
}
})
return res
}
// detect if blip in edit mode — clear all hl inside it
setInterval(function() {
// *for root-blip condition is different
if (cur_blip.is('.edit-mode') || cur_blip.is('.root-blip') && cur_blip.find('> .js-editor-container > .js-editor').prop('contenteditable') == 'true') {
find_hl_nodes(cur_blip).each(function() {
var node = $(this).parent()
tree.clear_node_hl(node)
tree.nodes_with_hl.pull(node)
})
}
}, 100)
// other events
$win.on('tree.clear_hl', function() {
search.cont.removeClass('_showing_results')
search.results.fadeOut()
})
}) // on header ready
}) // DOM ready
// helpers
RegExp.escape = function(str) {
return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1')
}
$.key = {
Enter: 13,
Esc: 27,
Slash: 191,
Slash_cyr: 190,
Slash_num: 111,
Down: 40,
Up: 38,
Left: 37,
Right: 39,
Tab: 9,
Space: 32,
PageUp: 33,
PageDown: 34,
Home: 36,
End: 35,
J: 74,
K: 75,
S: 83,
Shift: 16,
Ctrl: 17,
Del: 46,
}
Array.prototype.pull = function() {
var arg, args, index, j, len, output;
args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
output = [];
for (j = 0, len = args.length; j < len; j++) {
arg = args[j];
index = this.indexOf(arg);
if (index !== -1) {
output.push(this.splice(index, 1)[0]);
}
}
if (args.length === 1) {
output = output[0];
}
return output;
};
$.fn.getScrollTopMax = function() {
var el = this[0]
return el.scrollTopMax || el.scrollHeight - el.clientHeight
}
}(typeof unsafeWindow == 'undefined' ? window : unsafeWindow)