User:Inductiveload/MiniPane.js
Jump to navigation
Jump to search
Note: After saving, changes may not occur immediately. Click here to learn how to bypass your browser's cache.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Cmd-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (Cmd-Shift-R on a Mac)
- Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Clear the cache in Tools → Preferences
For details and instructions about other browsers, see Wikipedia:Bypass your cache.
Code that you insert on this page could contain malicious content capable of compromising your account. If you are unsure whether code you are adding to this page is safe, you can ask at the central discussion page, Scriptorium. The code will be executed when previewing this page under some skins, including Monobook. You can in the interim if you wish to refresh the content sooner under another skin. |
This script seems to have a documentation page at User:Inductiveload/MiniPane. |
/**
* MiniPane
*
* Script to make the proofreading interface a bit less horrible...maybe
*
* Changelog:
*/
'use strict';
// IIFE used when including as a user script (to allow debug or config)
// Default gadget use will get an IIFE wrapper as well
( function () {
const gadgetName = 'mini_pane',
MiniPane = {
dbName: 'enws-gadget-' + gadgetName,
enabled: true,
configured: false,
pageEntryExpiry: 48 * 60 * 60 * 1000, // 48h
indexEntryExpiry: 28 * 24 * 60 * 60 * 1000, // one month
scrollStep: 25,
zoomStep: 20,
paneHeight: 18, // in em
followGap: 45, // px
followMouse: true
};
function getTitle() {
return mw.config.get( 'wgTitle' );
}
function getIndex() {
return getTitle().replace( /\/[0-9]+$/, '' );
}
function imageMutated( mutations ) {
const mut = mutations[ mutations.length - 1 ];
const $tgt = $( mut.target );
const title = getTitle();
const loc = {
title: title,
left: $tgt.css( 'left' ),
top: $tgt.css( 'top' ),
width: $tgt.css( 'width' ),
lastUsed: new Date().getTime()
};
const pagePosStoreTrans = MiniPane.db
.transaction( [ 'pages', 'indexes' ], 'readwrite' );
pagePosStoreTrans.objectStore( 'pages' ).put( loc );
// and update the index
loc.title = getIndex();
loc.top = 0;
pagePosStoreTrans.objectStore( 'indexes' ).put( loc );
}
/* Trim old entries from the IndexedDB */
// https://github.com/whatwg/storage/issues/11
function tidyDb() {
const now = new Date().getTime();
const expireEntries = function ( index, expiry ) {
index.onsuccess = function ( event ) {
const cursor = event.target.result;
if ( cursor && ( cursor.value.lastUsed - now ) > expiry ) {
cursor.delete();
}
cursor.continue();
};
};
const pagePosStoreTrans = MiniPane.db
.transaction( [ 'pages', 'indexes' ], 'readwrite' );
let index = pagePosStoreTrans
.objectStore( 'pages' )
.index( 'lastUsed' );
expireEntries( index, MiniPane.pageEntryExpiry );
index = pagePosStoreTrans
.objectStore( 'indexes' )
.index( 'lastUsed' );
expireEntries( index, MiniPane.indexEntryExpiry );
}
function afterDB() {
// eslint-disable-next-line no-jquery/no-global-selector
const $pageImg = $( '.prp-page-image' );
const $ppImg = $pageImg.find( 'img' );
let enabled = false;
const $imgLoupe = $( '<div>' )
.addClass( 'ws-js-minipane' )
.addClass( 'ws-js-minipane prp-page-image-follower' )
.attr( 'id', 'ws-userjs-imgloupe' )
.css( {
position: 'absolute',
overflow: 'hidden',
height: MiniPane.paneHeight + 'em',
width: '50%',
'margin-bottom': '1.5em',
'z-index': 1000,
border: '1px solid grey',
'border-radius': '4px',
'box-shadow': 'rgba(50, 50, 93, 0.25) 0 6px 12px -2px, rgba(0, 0, 0, 0.3) 0 3px 7px -3px',
opacity: '90%',
top: 0
} )
.hide()
.insertAfter( '.prp-page-edit-header' );
const $img = $( '<img>' )
.attr( 'src', $ppImg.attr( 'src' ) )
.css( {
position: 'absolute',
width: '100%'
} )
.appendTo( $imgLoupe );
$( '<style>' )
.append( '.ws-js-minipane img:hover{ opacity: 80%; }' )
.appendTo( 'head' );
const observer = new MutationObserver( imageMutated );
// Start observing the target node for configured mutations
observer.observe( $img[ 0 ], {
attributes: true,
childList: false,
subtree: false
} );
const updateImagePos = function ( data ) {
if ( data ) {
$img.css( {
top: data.top,
left: data.left,
width: data.width
} );
}
};
const pagePosStoreTrans = MiniPane.db
.transaction( [ 'pages', 'indexes' ], 'readonly' );
let pagePosStoreReq = pagePosStoreTrans
.objectStore( 'pages' )
.get( getTitle() );
pagePosStoreReq.onsuccess = function ( event ) {
const data = event.target.result;
if ( data ) {
updateImagePos( event.target.result );
} else {
pagePosStoreReq = pagePosStoreTrans
.objectStore( 'indexes' )
.get( getIndex() );
pagePosStoreReq.onsuccess = function ( ievent ) {
updateImagePos( ievent.target.result );
};
}
};
let $textBox;
const preventOverscoll = function ( newTop, newLeft ) {
const maxLeft = $img.parent()[ 0 ].clientWidth - $img[ 0 ].clientWidth;
if ( newLeft ) {
if ( maxLeft > 0 ) {
// image smaller than box
newLeft = Math.min( maxLeft, Math.max( 0, newLeft ) );
} else if ( newLeft >= 0 ) {
newLeft = 0;
} else if ( newLeft < maxLeft ) {
newLeft = maxLeft;
}
$img.css( 'left', newLeft );
}
if ( newTop ) {
const maxTop = $img.parent()[ 0 ].clientHeight - $img[ 0 ].clientHeight;
if ( maxTop > 0 ) {
newTop = Math.min( maxTop, Math.max( 0, newTop ) );
} else if ( newTop >= 0 ) {
newTop = 0;
} else if ( newTop < maxTop ) {
newTop = maxTop;
}
$img.css( 'top', newTop );
}
};
const zoom = function ( zoomStep ) {
$img.css( {
width: '+=' + zoomStep,
left: '-=' + zoomStep / 2
} );
const pos = $img.position();
preventOverscoll( pos.top, pos.left );
};
const setTop = function ( newTop ) {
preventOverscoll( newTop, undefined );
};
const setLeft = function ( newLeft ) {
preventOverscoll( undefined, newLeft );
};
const scrollSideways = function ( step ) {
const pos = $img.position();
setLeft( pos.left + step );
};
const scrollUpDown = function ( step ) {
const pos = $img.position();
setTop( pos.top + step );
};
const centreAt = function ( x, y ) {
// var top = ( 0.5 - y ) * $img[ 0 ].clientHeight;
// var left = ( 0.5 - x ) * $img[ 0 ].clientWidth;
const distFromEdgeX = ( x * $img[ 0 ].clientWidth );
const distFromEdgeY = ( y * $img[ 0 ].clientHeight );
const left = ( $imgLoupe[ 0 ].clientWidth / 2 ) - distFromEdgeX;
const top = ( $imgLoupe[ 0 ].clientHeight / 2 ) - distFromEdgeY;
setTop( top );
setLeft( left );
};
const handleKeypress = function ( evt ) {
let prop = false;
if ( evt.code === 'Numpad2' ) {
scrollUpDown( -MiniPane.scrollStep );
} else if ( evt.code === 'Numpad8' ) {
scrollUpDown( MiniPane.scrollStep );
} else if ( evt.code === 'Numpad4' ) {
scrollSideways( MiniPane.scrollStep );
} else if ( evt.code === 'Numpad6' ) {
scrollSideways( -MiniPane.scrollStep );
} else if ( evt.code === 'NumpadAdd' ) {
zoom( MiniPane.zoomStep );
} else if ( evt.code === 'NumpadSubtract' ) {
zoom( -MiniPane.zoomStep );
} else if ( evt.shiftKey && evt.code === 'Enter' ) {
scrollUpDown( -MiniPane.scrollStep );
$textBox.scrollTop( $textBox.scrollTop() + 16 );
} else if ( evt.ctrlKey && evt.code === 'Enter' ) {
scrollUpDown( MiniPane.scrollStep );
$textBox.scrollTop( $textBox.scrollTop() - 16 );
} else if ( evt.code === 'Numpad3' ) {
scrollUpDown( ( -0.8 * MiniPane.paneHeight ) + 'em' );
} else if ( evt.code === 'Numpad9' ) {
scrollUpDown( ( 0.8 * MiniPane.paneHeight ) + 'em' );
} else {
prop = true;
}
if ( !prop ) {
evt.preventDefault();
}
};
// eslint-disable-next-line no-jquery/no-global-selector
$( 'body' ).on( 'keypress', handleKeypress );
const handleScroll = function ( event ) {
const down = event.originalEvent.deltaY < 0;
if ( event.shiftKey ) {
scrollSideways( ( down ? -1 : 1 ) * MiniPane.scrollStep );
} else if ( event.ctrlKey ) {
zoom( ( down ? 1 : -1 ) * MiniPane.zoomStep );
} else {
scrollUpDown( ( down ? 1 : -1 ) * MiniPane.scrollStep );
}
event.preventDefault();
};
const indirectScroll = function ( event ) {
if ( event.shiftKey || event.ctrlKey ) {
if ( !event.ctrlKey ) {
event.shiftKey = false;
}
handleScroll( event );
}
};
// scrolling on image
$imgLoupe.on( 'wheel', handleScroll );
$pageImg.on( 'wheel', indirectScroll );
let lastTop = 0;
function getTextBox() {
// eslint-disable-next-line no-jquery/no-global-selector
let $foundTextBox = $( '.CodeMirror' );
if ( $foundTextBox.length === 0 ) {
// eslint-disable-next-line no-jquery/no-global-selector
$foundTextBox = $( '#wpTextbox1' );
}
return $foundTextBox;
}
function showLoupe() {
if ( enabled ) {
$imgLoupe.show();
}
}
function hideLoupe() {
$imgLoupe.hide();
}
const followMouse = function ( event ) {
const $relativeToElement = $imgLoupe.parents( '.wikiEditor-ui-bottom' );
const tbrect = $textBox[ 0 ].getBoundingClientRect();
if ( event.clientY - tbrect.top < 0 ) {
// went above the text box
hideLoupe();
} else {
const rect = $relativeToElement[ 0 ].getBoundingClientRect();
showLoupe();
lastTop = event.clientY - rect.top -
$imgLoupe[ 0 ].clientHeight - MiniPane.followGap;
$imgLoupe.css( {
top: lastTop
} );
}
};
let boundOnce = false;
function bindTextboxHandlers() {
// first unbind the old handlers
$textBox.off( 'wheel', indirectScroll );
$textBox.off( 'focus', showLoupe );
$textBox.off( 'focusout mouseout', hideLoupe );
$textBox.off( 'mousemove', followMouse );
// update what the textbox is
$textBox = getTextBox();
$textBox.on( 'wheel', indirectScroll );
if ( MiniPane.followMouse ) {
$textBox.on( 'mousemove', followMouse );
} else {
// follow caret mode
// make image draggable
if ( !boundOnce ) {
$img.drags();
$imgLoupe.css( {
transition: 'top 0.5s ease 0s'
} );
}
// eslint-disable-next-line no-jquery/no-global-selector
const prpBody = $( '.prp-page-edit-body' )[ 0 ];
$textBox.on( 'keydown click focus scroll', function () {
const pos = getCaretCoordinates( this, this.selectionStart );
showLoupe();
if ( pos.top !== lastTop ) {
lastTop = pos.top + prpBody.offsetTop -
$imgLoupe[ 0 ].clientHeight - this.scrollTop;
$imgLoupe.css( {
top: lastTop
} );
}
} );
}
$textBox.on( 'focus', showLoupe );
$textBox.on( 'focusout mouseout', hideLoupe );
boundOnce = true;
}
$imgLoupe.on( 'mousemove', followMouse );
$pageImg.on( 'mousemove', followMouse );
$pageImg.on( 'mousemove', showLoupe );
$pageImg.on( 'mouseout', hideLoupe );
$imgLoupe.on( 'draglessClick', hideLoupe );
// handl highres loads
mw.hook( 'JumpToFile.highres_set' ).add( function ( hires ) {
$img.attr( 'src', hires.href );
} );
$pageImg.on( 'draglessClick', function ( event ) {
if ( event.ctrlKey ) {
const pos = $ppImg[ 0 ].getBoundingClientRect();
const x = ( event.clientX - pos.left ) / pos.width;
const y = ( event.clientY - pos.top ) / pos.height;
centreAt( x, y );
}
} );
/* rebind the handlers on codemirror creation/destruction */
mw.hook( 'ext.CodeMirror.switch' ).add( function () {
bindTextboxHandlers();
} );
// bind to the current TB
$textBox = getTextBox();
bindTextboxHandlers();
function setUpEnableToggle() {
enabled = mw.storage.get( 'ext.gadget.minipane.enable' ) === '1';
const getText = function ( active ) {
return ( active ? 'Disable' : 'Enable' ) + ' MiniPane.js';
};
const toggleLink = mw.util.addPortletLink(
'p-tb',
'#',
getText( enabled ),
'n-enableMiniPane',
'Enable/disable MiniPane gadget'
);
toggleLink.onclick = function ( event ) {
enabled = !enabled;
mw.storage.set( 'ext.gadget.minipane.enable', enabled ? '1' : '0' );
// eslint-disable-next-line no-jquery/no-global-selector
$( '#n-enableMiniPane' ).find( 'a' ).text( getText( enabled ) );
if ( enabled ) {
showLoupe();
} else {
hideLoupe();
}
event.preventDefault();
};
if ( enabled ) {
showLoupe();
}
}
setUpEnableToggle();
// finally, clean old entries
tidyDb();
}
function init() {
console.log( 'Init ' + gadgetName );
const request = window.indexedDB.open( MiniPane.dbName, 1 );
request.onerror = function ( event ) {
console.error( event );
alert( 'IndexedDB error: ' + request.errorCode );
};
request.onsuccess = function ( event ) {
MiniPane.db = event.target.result;
MiniPane.db.onerror = function ( errEvent ) {
console.error( 'Database error: ' + errEvent.target.errorCode );
};
afterDB();
};
request.onupgradeneeded = function ( event ) {
const db = event.target.result;
// Create an objectStore for this database
let objectStore = db.createObjectStore( 'pages', { keyPath: 'title' } );
objectStore.createIndex( 'title', 'title', { unique: true } );
objectStore.createIndex( 'top', 'top', { unique: false } );
objectStore.createIndex( 'left', 'left', { unique: false } );
objectStore.createIndex( 'width', 'width', { unique: false } );
objectStore.createIndex( 'lastUsed', 'lastUsed', { unique: false } );
objectStore = db.createObjectStore( 'indexes', { keyPath: 'title' } );
objectStore.createIndex( 'title', 'title', { unique: true } );
objectStore.createIndex( 'top', 'top', { unique: false } );
objectStore.createIndex( 'left', 'left', { unique: false } );
objectStore.createIndex( 'width', 'width', { unique: false } );
objectStore.createIndex( 'lastUsed', 'lastUsed', { unique: false } );
objectStore.transaction.oncomplete = function () {
// afterDB();
};
};
}
mw.loader.load( 'mediawiki.storage' );
$( function () {
if ( mw.config.get( 'wgCanonicalNamespace' ) === 'Page' &&
[ 'edit', 'submit' ].indexOf( mw.config.get( 'wgAction' ) ) !== -1 ) {
init();
}
} );
}() );
( function ( $ ) {
$.fn.drags = function ( opt ) {
opt = $.extend( { handle: '', cursor: 'move' }, opt );
if ( opt.handle === '' ) {
var $el = this;
} else {
var $el = this.find( opt.handle );
}
return $el.css( 'cursor', opt.cursor ).on( 'mousedown', function ( e ) {
if ( opt.handle === '' ) {
var $drag = $( this ).addClass( 'draggable' );
} else {
var $drag = $( this ).addClass( 'active-handle' ).parent().addClass( 'draggable' );
}
const z_idx = $drag.css( 'z-index' ),
drg_h = $drag.outerHeight(),
drg_w = $drag.outerWidth(),
pos_y = $drag.offset().top + drg_h - e.pageY,
pos_x = $drag.offset().left + drg_w - e.pageX;
$drag.css( 'z-index', 1000 ).parents().on( 'mousemove', function ( e ) {
$( '.draggable' ).offset( {
top: e.pageY + pos_y - drg_h,
left: e.pageX + pos_x - drg_w
} ).on( 'mouseup', function () {
$( this ).removeClass( 'draggable' ).css( 'z-index', z_idx );
} );
} );
e.preventDefault(); // disable selection
} ).on( 'mouseup mouseout', function () {
if ( opt.handle === '' ) {
$( this ).removeClass( 'draggable' );
} else {
$( this ).removeClass( 'active-handle' ).parent().removeClass( 'draggable' );
}
} );
};
}( jQuery ) );
/* jshint browser: true */
( function () {
// We'll copy the properties below into the mirror div.
// Note that some browsers, such as Firefox, do not concatenate properties
// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
// so we have to list every single property explicitly.
const properties = [
'direction', // RTL support
'boxSizing',
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
'height',
'overflowX',
'overflowY', // copy the scrollbar for IE
'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
'borderLeftWidth',
'borderStyle',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
'fontStyle',
'fontVariant',
'fontWeight',
'fontStretch',
'fontSize',
'fontSizeAdjust',
'lineHeight',
'fontFamily',
'textAlign',
'textTransform',
'textIndent',
'textDecoration', // might not make a difference, but better be safe
'letterSpacing',
'wordSpacing',
'tabSize',
'MozTabSize'
];
const isBrowser = ( typeof window !== 'undefined' );
const isFirefox = ( isBrowser && window.mozInnerScreenX != null );
function getCaretCoordinates( element, position, options ) {
if ( !isBrowser ) {
throw new Error( 'textarea-caret-position#getCaretCoordinates should only be called in a browser' );
}
const debug = options && options.debug || false;
if ( debug ) {
const el = document.querySelector( '#input-textarea-caret-position-mirror-div' );
if ( el ) { el.parentNode.removeChild( el ); }
}
// The mirror div will replicate the textarea's style
const div = document.createElement( 'div' );
div.id = 'input-textarea-caret-position-mirror-div';
document.body.appendChild( div );
const style = div.style;
const computed = window.getComputedStyle ? window.getComputedStyle( element ) : element.currentStyle; // currentStyle for IE < 9
const isInput = element.nodeName === 'INPUT';
// Default textarea styles
style.whiteSpace = 'pre-wrap';
if ( !isInput ) { style.wordWrap = 'break-word'; } // only for textarea-s
// Position off-screen
style.position = 'absolute'; // required to return coordinates properly
if ( !debug ) { style.visibility = 'hidden'; } // not 'display: none' because we want rendering
// Transfer the element's properties to the div
properties.forEach( function ( prop ) {
if ( isInput && prop === 'lineHeight' ) {
// Special case for <input>s because text is rendered centered and line height may be != height
if ( computed.boxSizing === 'border-box' ) {
const height = parseInt( computed.height );
const outerHeight =
parseInt( computed.paddingTop ) +
parseInt( computed.paddingBottom ) +
parseInt( computed.borderTopWidth ) +
parseInt( computed.borderBottomWidth );
const targetHeight = outerHeight + parseInt( computed.lineHeight );
if ( height > targetHeight ) {
style.lineHeight = height - outerHeight + 'px';
} else if ( height === targetHeight ) {
style.lineHeight = computed.lineHeight;
} else {
style.lineHeight = 0;
}
} else {
style.lineHeight = computed.height;
}
} else {
style[ prop ] = computed[ prop ];
}
} );
if ( isFirefox ) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if ( element.scrollHeight > parseInt( computed.height ) ) { style.overflowY = 'scroll'; }
} else {
style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}
div.textContent = element.value.substring( 0, position );
// The second special handling for input type="text" vs textarea:
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
if ( isInput ) { div.textContent = div.textContent.replace( /\s/g, '\u00a0' ); }
const span = document.createElement( 'span' );
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
// For inputs, just '.' would be enough, but no need to bother.
span.textContent = element.value.substring( position ) || '.'; // || because a completely empty faux span doesn't render at all
div.appendChild( span );
const coordinates = {
top: span.offsetTop + parseInt( computed.borderTopWidth ),
left: span.offsetLeft + parseInt( computed.borderLeftWidth ),
height: parseInt( computed.lineHeight )
};
if ( debug ) {
span.style.backgroundColor = '#aaa';
} else {
document.body.removeChild( div );
}
return coordinates;
}
if ( typeof module !== 'undefined' && typeof module.exports !== 'undefined' ) {
module.exports = getCaretCoordinates;
} else if ( isBrowser ) {
window.getCaretCoordinates = getCaretCoordinates;
}
}() );
/**
* Better jQuery click event that's not invoked when you drag or select text
*
* Copyright (C) 2018 Jakub T. Jankiewicz <https://jcubic.pl/me>
* Released under MIT license
*
* solution based on this SO question
* https://stackoverflow.com/a/21851799/387194
*/
/* global jQuery, setTimeout, clearTimeout, define, module, exports */
( function ( factory ) {
if ( typeof define === 'function' && define.amd ) {
// AMD. Register as an anonymous module.
define( [ 'jquery' ], factory );
} else if ( typeof exports === 'object' ) {
// Node/CommonJS style for Browserify
module.exports = factory;
} else {
// Browser globals
factory( jQuery );
}
}( function ( $ ) {
$.event.special.draglessClick = {
setup: function () {
console.log( 'setup' );
const $element = $( this );
const callbacks = $.Callbacks();
let isDragging = false;
let timer;
var handlers = {
move: function mousemove() {
isDragging = true;
$( window ).off( 'mousemove', handlers.move );
},
down: function () {
isDragging = false;
// there is wierd issue where move is triggerd just
// after mousedown even without moving the cursor
timer = setTimeout( function () {
$( window ).on( 'mousemove', handlers.move );
}, 100 );
},
up: function () {
clearTimeout( timer );
$( window ).off( 'mousemove', handlers.move );
},
click: function ( e ) {
const wasDragging = isDragging;
isDragging = false;
if ( !wasDragging ) {
callbacks.fireWith( this, [ e ] );
}
}
};
$element
.data( 'handlers', handlers )
.data( 'callbacks', callbacks )
.on( 'mousedown', handlers.down )
.on( 'mouseup', handlers.up )
.on( 'click', handlers.click );
},
teardown: function () {
const $element = $( this );
const callbacks = $element.data( 'callbacks' );
callbacks.empty();
$element.removeData( 'callbacks' );
const handlers = $element.data( 'handlers' );
if ( handlers ) {
$( window ).off( 'mousemove', handlers.move );
}
$element
.off( 'mousedown', handlers.down )
.off( 'mouseup', handlers.up )
.off( 'click', handlers.click );
},
add: function ( handlerObject ) {
$( this ).data( 'callbacks' ).add( handlerObject.handler );
},
remove: function ( handlerObject ) {
$( this ).data( 'callbacks' ).remove( handlerObject.handler );
},
trigger: function ( e, data ) {
const event = $.Event( 'click' );
$( this ).data( 'callbacks' ).fireWith( this, [ event ] );
}
};
} ) );