12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830 |
- //
- // YYTextView.m
- // YYText <https://github.com/ibireme/YYText>
- //
- // Created by ibireme on 15/2/25.
- // Copyright (c) 2015 ibireme.
- //
- // This source code is licensed under the MIT-style license found in the
- // LICENSE file in the root directory of this source tree.
- //
- #import "YYTextView.h"
- #import "YYTextInput.h"
- #import "YYTextContainerView.h"
- #import "YYTextSelectionView.h"
- #import "YYTextMagnifier.h"
- #import "YYTextEffectWindow.h"
- #import "YYTextKeyboardManager.h"
- #import "YYTextUtilities.h"
- #import "YYTextTransaction.h"
- #import "YYTextWeakProxy.h"
- #import "NSAttributedString+YYText.h"
- #import "UIPasteboard+YYText.h"
- #import "UIView+YYText.h"
- static double _YYDeviceSystemVersion() {
- static double version;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- version = [UIDevice currentDevice].systemVersion.doubleValue;
- });
- return version;
- }
- #ifndef kSystemVersion
- #define kSystemVersion _YYDeviceSystemVersion()
- #endif
- #ifndef kiOS6Later
- #define kiOS6Later (kSystemVersion >= 6)
- #endif
- #ifndef kiOS7Later
- #define kiOS7Later (kSystemVersion >= 7)
- #endif
- #ifndef kiOS8Later
- #define kiOS8Later (kSystemVersion >= 8)
- #endif
- #ifndef kiOS9Later
- #define kiOS9Later (kSystemVersion >= 9)
- #endif
- #define kDefaultUndoLevelMax 20 // Default maximum undo level
- #define kAutoScrollMinimumDuration 0.1 // Time in seconds to tick auto-scroll.
- #define kLongPressMinimumDuration 0.5 // Time in seconds the fingers must be held down for long press gesture.
- #define kLongPressAllowableMovement 10.0 // Maximum movement in points allowed before the long press fails.
- #define kMagnifierRangedTrackFix -6.0 // Magnifier ranged offset fix.
- #define kMagnifierRangedPopoverOffset 4.0 // Magnifier ranged popover offset.
- #define kMagnifierRangedCaptureOffset -6.0 // Magnifier ranged capture center offset.
- #define kHighlightFadeDuration 0.15 // Time in seconds for highlight fadeout animation.
- #define kDefaultInset UIEdgeInsetsMake(6, 4, 6, 4)
- #define kDefaultVerticalInset UIEdgeInsetsMake(4, 6, 4, 6)
- NSString *const YYTextViewTextDidBeginEditingNotification = @"YYTextViewTextDidBeginEditing";
- NSString *const YYTextViewTextDidChangeNotification = @"YYTextViewTextDidChange";
- NSString *const YYTextViewTextDidEndEditingNotification = @"YYTextViewTextDidEndEditing";
- typedef NS_ENUM (NSUInteger, YYTextGrabberDirection) {
- kStart = 1,
- kEnd = 2,
- };
- typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
- kLeft = 1,
- kTop = 2,
- kRight = 3,
- kBottom = 4,
- };
- /// An object that captures the state of the text view. Used for undo and redo.
- @interface _YYTextViewUndoObject : NSObject
- @property (nonatomic, strong) NSAttributedString *text;
- @property (nonatomic, assign) NSRange selectedRange;
- @end
- @implementation _YYTextViewUndoObject
- + (instancetype)objectWithText:(NSAttributedString *)text range:(NSRange)range {
- _YYTextViewUndoObject *obj = [self new];
- obj.text = text ? text : [NSAttributedString new];
- obj.selectedRange = range;
- return obj;
- }
- @end
- @interface YYTextView () <UIScrollViewDelegate, UIAlertViewDelegate, YYTextDebugTarget, YYTextKeyboardObserver> {
-
- YYTextRange *_selectedTextRange; /// nonnull
- YYTextRange *_markedTextRange;
-
- __weak id<YYTextViewDelegate> _outerDelegate;
-
- UIImageView *_placeHolderView;
-
- NSMutableAttributedString *_innerText; ///< nonnull, inner attributed text
- NSMutableAttributedString *_delectedText; ///< detected text for display
- YYTextContainer *_innerContainer; ///< nonnull, inner text container
- YYTextLayout *_innerLayout; ///< inner text layout, the text in this layout is longer than `_innerText` by appending '\n'
-
- YYTextContainerView *_containerView; ///< nonnull
- YYTextSelectionView *_selectionView; ///< nonnull
- YYTextMagnifier *_magnifierCaret; ///< nonnull
- YYTextMagnifier *_magnifierRanged; ///< nonnull
-
- NSMutableAttributedString *_typingAttributesHolder; ///< nonnull, typing attributes
- NSDataDetector *_dataDetector;
- CGFloat _magnifierRangedOffset;
-
- NSRange _highlightRange; ///< current highlight range
- YYTextHighlight *_highlight; ///< highlight attribute in `_highlightRange`
- YYTextLayout *_highlightLayout; ///< when _state.showingHighlight=YES, this layout should be displayed
- YYTextRange *_trackingRange; ///< the range in _innerLayout, may out of _innerText.
-
- BOOL _insetModifiedByKeyboard; ///< text is covered by keyboard, and the contentInset is modified
- UIEdgeInsets _originalContentInset; ///< the original contentInset before modified
- UIEdgeInsets _originalScrollIndicatorInsets; ///< the original scrollIndicatorInsets before modified
-
- NSTimer *_longPressTimer;
- NSTimer *_autoScrollTimer;
- CGFloat _autoScrollOffset; ///< current auto scroll offset which shoud add to scroll view
- NSInteger _autoScrollAcceleration; ///< an acceleration coefficient for auto scroll
- NSTimer *_selectionDotFixTimer; ///< fix the selection dot in window if the view is moved by parents
- CGPoint _previousOriginInWindow;
-
- CGPoint _touchBeganPoint;
- CGPoint _trackingPoint;
- NSTimeInterval _touchBeganTime;
- NSTimeInterval _trackingTime;
-
- NSMutableArray *_undoStack;
- NSMutableArray *_redoStack;
- NSRange _lastTypeRange;
-
- struct {
- unsigned int trackingGrabber : 2; ///< YYTextGrabberDirection, current tracking grabber
- unsigned int trackingCaret : 1; ///< track the caret
- unsigned int trackingPreSelect : 1; ///< track pre-select
- unsigned int trackingTouch : 1; ///< is in touch phase
- unsigned int swallowTouch : 1; ///< don't forward event to next responder
- unsigned int touchMoved : 3; ///< YYTextMoveDirection, move direction after touch began
- unsigned int selectedWithoutEdit : 1; ///< show selected range but not first responder
- unsigned int deleteConfirm : 1; ///< delete a binding text range
- unsigned int ignoreFirstResponder : 1; ///< ignore become first responder temporary
- unsigned int ignoreTouchBegan : 1; ///< ignore begin tracking touch temporary
-
- unsigned int showingMagnifierCaret : 1;
- unsigned int showingMagnifierRanged : 1;
- unsigned int showingMenu : 1;
- unsigned int showingHighlight : 1;
-
- unsigned int typingAttributesOnce : 1; ///< apply the typing attributes once
- unsigned int clearsOnInsertionOnce : 1; ///< select all once when become first responder
- unsigned int autoScrollTicked : 1; ///< auto scroll did tick scroll at this timer period
- unsigned int firstShowDot : 1; ///< the selection grabber dot has displayed at least once
- unsigned int needUpdate : 1; ///< the layout or selection view is 'dirty' and need update
- unsigned int placeholderNeedUpdate : 1; ///< the placeholder need update it's contents
-
- unsigned int insideUndoBlock : 1;
- unsigned int firstResponderBeforeUndoAlert : 1;
- } _state;
- }
- @end
- @implementation YYTextView
- #pragma mark - @protocol UITextInputTraits
- @synthesize autocapitalizationType = _autocapitalizationType;
- @synthesize autocorrectionType = _autocorrectionType;
- @synthesize spellCheckingType = _spellCheckingType;
- @synthesize keyboardType = _keyboardType;
- @synthesize keyboardAppearance = _keyboardAppearance;
- @synthesize returnKeyType = _returnKeyType;
- @synthesize enablesReturnKeyAutomatically = _enablesReturnKeyAutomatically;
- @synthesize secureTextEntry = _secureTextEntry;
- #pragma mark - @protocol UITextInput
- @synthesize selectedTextRange = _selectedTextRange; //copy nonnull (YYTextRange*)
- @synthesize markedTextRange = _markedTextRange; //readonly (YYTextRange*)
- @synthesize markedTextStyle = _markedTextStyle; //copy
- @synthesize inputDelegate = _inputDelegate; //assign
- @synthesize tokenizer = _tokenizer; //readonly
- #pragma mark - @protocol UITextInput optional
- @synthesize selectionAffinity = _selectionAffinity;
- #pragma mark - Private
- /// Update layout and selection before runloop sleep/end.
- - (void)_commitUpdate {
- #if !TARGET_INTERFACE_BUILDER
- _state.needUpdate = YES;
- [[YYTextTransaction transactionWithTarget:self selector:@selector(_updateIfNeeded)] commit];
- #else
- [self _update];
- #endif
- }
- /// Update layout and selection view if needed.
- - (void)_updateIfNeeded {
- if (_state.needUpdate) {
- [self _update];
- }
- }
- /// Update layout and selection view immediately.
- - (void)_update {
- _state.needUpdate = NO;
- [self _updateLayout];
- [self _updateSelectionView];
- }
- /// Update layout immediately.
- - (void)_updateLayout {
- NSMutableAttributedString *text = _innerText.mutableCopy;
- _placeHolderView.hidden = text.length > 0;
- if ([self _detectText:text]) {
- _delectedText = text;
- } else {
- _delectedText = nil;
- }
- [text replaceCharactersInRange:NSMakeRange(text.length, 0) withString:@"\r"]; // add for nextline caret
- [text yy_removeDiscontinuousAttributesInRange:NSMakeRange(_innerText.length, 1)];
- [text removeAttribute:YYTextBorderAttributeName range:NSMakeRange(_innerText.length, 1)];
- [text removeAttribute:YYTextBackgroundBorderAttributeName range:NSMakeRange(_innerText.length, 1)];
- if (_innerText.length == 0) {
- [text yy_setAttributes:_typingAttributesHolder.yy_attributes]; // add for empty text caret
- }
- if (_selectedTextRange.end.offset == _innerText.length) {
- [_typingAttributesHolder.yy_attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
- [text yy_setAttribute:key value:value range:NSMakeRange(_innerText.length, 1)];
- }];
- }
- [self willChangeValueForKey:@"textLayout"];
- _innerLayout = [YYTextLayout layoutWithContainer:_innerContainer text:text];
- [self didChangeValueForKey:@"textLayout"];
- CGSize size = [_innerLayout textBoundingSize];
- CGSize visibleSize = [self _getVisibleSize];
- if (_innerContainer.isVerticalForm) {
- size.height = visibleSize.height;
- if (size.width < visibleSize.width) size.width = visibleSize.width;
- } else {
- size.width = visibleSize.width;
- }
-
- [_containerView setLayout:_innerLayout withFadeDuration:0];
- _containerView.frame = (CGRect){.size = size};
- _state.showingHighlight = NO;
- self.contentSize = size;
- }
- /// Update selection view immediately.
- /// This method should be called after "layout update" finished.
- - (void)_updateSelectionView {
- _selectionView.frame = _containerView.frame;
- _selectionView.caretBlinks = NO;
- _selectionView.caretVisible = NO;
- _selectionView.selectionRects = nil;
- [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
- if (!_innerLayout) return;
-
- NSMutableArray *allRects = [NSMutableArray new];
- BOOL containsDot = NO;
-
- YYTextRange *selectedRange = _selectedTextRange;
- if (_state.trackingTouch && _trackingRange) {
- selectedRange = _trackingRange;
- }
-
- if (_markedTextRange) {
- NSArray *rects = [_innerLayout selectionRectsWithoutStartAndEndForRange:_markedTextRange];
- if (rects) [allRects addObjectsFromArray:rects];
- if (selectedRange.asRange.length > 0) {
- rects = [_innerLayout selectionRectsWithOnlyStartAndEndForRange:selectedRange];
- if (rects) [allRects addObjectsFromArray:rects];
- containsDot = rects.count > 0;
- } else {
- CGRect rect = [_innerLayout caretRectForPosition:selectedRange.end];
- _selectionView.caretRect = [self _convertRectFromLayout:rect];
- _selectionView.caretVisible = YES;
- _selectionView.caretBlinks = YES;
- }
- } else {
- if (selectedRange.asRange.length == 0) { // only caret
- if (self.isFirstResponder || _state.trackingPreSelect) {
- CGRect rect = [_innerLayout caretRectForPosition:selectedRange.end];
- _selectionView.caretRect = [self _convertRectFromLayout:rect];
- _selectionView.caretVisible = YES;
- if (!_state.trackingCaret && !_state.trackingPreSelect) {
- _selectionView.caretBlinks = YES;
- }
- }
- } else { // range selected
- if ((self.isFirstResponder && !_state.deleteConfirm) ||
- (!self.isFirstResponder && _state.selectedWithoutEdit)) {
- NSArray *rects = [_innerLayout selectionRectsForRange:selectedRange];
- if (rects) [allRects addObjectsFromArray:rects];
- containsDot = rects.count > 0;
- } else if ((!self.isFirstResponder && _state.trackingPreSelect) ||
- (self.isFirstResponder && _state.deleteConfirm)){
- NSArray *rects = [_innerLayout selectionRectsWithoutStartAndEndForRange:selectedRange];
- if (rects) [allRects addObjectsFromArray:rects];
- }
- }
- }
- [allRects enumerateObjectsUsingBlock:^(YYTextSelectionRect *rect, NSUInteger idx, BOOL *stop) {
- rect.rect = [self _convertRectFromLayout:rect.rect];
- }];
- _selectionView.selectionRects = allRects;
- if (!_state.firstShowDot && containsDot) {
- _state.firstShowDot = YES;
- /*
- The dot position may be wrong at the first time displayed.
- I can't find the reason. Here's a workaround.
- */
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.02 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
- });
- }
- [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
-
- if (containsDot) {
- [self _startSelectionDotFixTimer];
- } else {
- [self _endSelectionDotFixTimer];
- }
- }
- /// Update inner contains's size.
- - (void)_updateInnerContainerSize {
- CGSize size = [self _getVisibleSize];
- if (_innerContainer.isVerticalForm) size.width = CGFLOAT_MAX;
- else size.height = CGFLOAT_MAX;
- _innerContainer.size = size;
- }
- /// Update placeholder before runloop sleep/end.
- - (void)_commitPlaceholderUpdate {
- #if !TARGET_INTERFACE_BUILDER
- _state.placeholderNeedUpdate = YES;
- [[YYTextTransaction transactionWithTarget:self selector:@selector(_updatePlaceholderIfNeeded)] commit];
- #else
- [self _updatePlaceholder];
- #endif
- }
- /// Update placeholder if needed.
- - (void)_updatePlaceholderIfNeeded {
- if (_state.placeholderNeedUpdate) {
- _state.placeholderNeedUpdate = NO;
- [self _updatePlaceholder];
- }
- }
- /// Update placeholder immediately.
- - (void)_updatePlaceholder {
- CGRect frame = CGRectZero;
- _placeHolderView.image = nil;
- _placeHolderView.frame = frame;
- if (_placeholderAttributedText.length > 0) {
- YYTextContainer *container = _innerContainer.copy;
- container.size = self.bounds.size;
- container.truncationType = YYTextTruncationTypeEnd;
- container.truncationToken = nil;
- YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_placeholderAttributedText];
- CGSize size = [layout textBoundingSize];
- BOOL needDraw = size.width > 1 && size.height > 1;
- if (needDraw) {
- UIGraphicsBeginImageContextWithOptions(size, NO, 0);
- CGContextRef context = UIGraphicsGetCurrentContext();
- [layout drawInContext:context size:size debug:self.debugOption];
- UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
- _placeHolderView.image = image;
- frame.size = image.size;
- if (container.isVerticalForm) {
- frame.origin.x = self.bounds.size.width - image.size.width;
- } else {
- frame.origin = CGPointZero;
- }
- _placeHolderView.frame = frame;
- }
- }
- }
- /// Update the `_selectedTextRange` to a single position by `_trackingPoint`.
- - (void)_updateTextRangeByTrackingCaret {
- if (!_state.trackingTouch) return;
-
- CGPoint trackingPoint = [self _convertPointToLayout:_trackingPoint];
- YYTextPosition *newPos = [_innerLayout closestPositionToPoint:trackingPoint];
- if (newPos) {
- newPos = [self _correctedTextPosition:newPos];
- if (_markedTextRange) {
- if ([newPos compare:_markedTextRange.start] == NSOrderedAscending) {
- newPos = _markedTextRange.start;
- } else if ([newPos compare:_markedTextRange.end] == NSOrderedDescending) {
- newPos = _markedTextRange.end;
- }
- }
- YYTextRange *newRange = [YYTextRange rangeWithRange:NSMakeRange(newPos.offset, 0) affinity:newPos.affinity];
- _trackingRange = newRange;
- }
- }
- /// Update the `_selectedTextRange` to a new range by `_trackingPoint` and `_state.trackingGrabber`.
- - (void)_updateTextRangeByTrackingGrabber {
- if (!_state.trackingTouch || !_state.trackingGrabber) return;
-
- BOOL isStart = _state.trackingGrabber == kStart;
- CGPoint magPoint = _trackingPoint;
- magPoint.y += kMagnifierRangedTrackFix;
- magPoint = [self _convertPointToLayout:magPoint];
- YYTextPosition *position = [_innerLayout positionForPoint:magPoint
- oldPosition:(isStart ? _selectedTextRange.start : _selectedTextRange.end)
- otherPosition:(isStart ? _selectedTextRange.end : _selectedTextRange.start)];
- if (position) {
- position = [self _correctedTextPosition:position];
- if ((NSUInteger)position.offset > _innerText.length) {
- position = [YYTextPosition positionWithOffset:_innerText.length];
- }
- YYTextRange *newRange = [YYTextRange rangeWithStart:(isStart ? position : _selectedTextRange.start)
- end:(isStart ? _selectedTextRange.end : position)];
- _trackingRange = newRange;
- }
- }
- /// Update the `_selectedTextRange` to a new range/position by `_trackingPoint`.
- - (void)_updateTextRangeByTrackingPreSelect {
- if (!_state.trackingTouch) return;
- YYTextRange *newRange = [self _getClosestTokenRangeAtPoint:_trackingPoint];
- _trackingRange = newRange;
- }
- /// Show or update `_magnifierCaret` based on `_trackingPoint`, and hide `_magnifierRange`.
- - (void)_showMagnifierCaret {
- if (YYTextIsAppExtension()) return;
-
- if (_state.showingMagnifierRanged) {
- _state.showingMagnifierRanged = NO;
- [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged];
- }
-
- _magnifierCaret.hostPopoverCenter = _trackingPoint;
- _magnifierCaret.hostCaptureCenter = _trackingPoint;
- if (!_state.showingMagnifierCaret) {
- _state.showingMagnifierCaret = YES;
- [[YYTextEffectWindow sharedWindow] showMagnifier:_magnifierCaret];
- } else {
- [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierCaret];
- }
- }
- /// Show or update `_magnifierRanged` based on `_trackingPoint`, and hide `_magnifierCaret`.
- - (void)_showMagnifierRanged {
- if (YYTextIsAppExtension()) return;
-
- if (_verticalForm) { // hack for vertical form...
- [self _showMagnifierCaret];
- return;
- }
-
- if (_state.showingMagnifierCaret) {
- _state.showingMagnifierCaret = NO;
- [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret];
- }
-
- CGPoint magPoint = _trackingPoint;
- if (_verticalForm) {
- magPoint.x += kMagnifierRangedTrackFix;
- } else {
- magPoint.y += kMagnifierRangedTrackFix;
- }
-
- YYTextRange *selectedRange = _selectedTextRange;
- if (_state.trackingTouch && _trackingRange) {
- selectedRange = _trackingRange;
- }
-
- YYTextPosition *position;
- if (_markedTextRange) {
- position = selectedRange.end;
- } else {
- position = [_innerLayout positionForPoint:[self _convertPointToLayout:magPoint]
- oldPosition:(_state.trackingGrabber == kStart ? selectedRange.start : selectedRange.end)
- otherPosition:(_state.trackingGrabber == kStart ? selectedRange.end : selectedRange.start)];
- }
-
- NSUInteger lineIndex = [_innerLayout lineIndexForPosition:position];
- if (lineIndex < _innerLayout.lines.count) {
- YYTextLine *line = _innerLayout.lines[lineIndex];
- CGRect lineRect = [self _convertRectFromLayout:line.bounds];
- if (_verticalForm) {
- magPoint.x = YYTEXT_CLAMP(magPoint.x, CGRectGetMinX(lineRect), CGRectGetMaxX(lineRect));
- } else {
- magPoint.y = YYTEXT_CLAMP(magPoint.y, CGRectGetMinY(lineRect), CGRectGetMaxY(lineRect));
- }
- CGPoint linePoint = [_innerLayout linePositionForPosition:position];
- linePoint = [self _convertPointFromLayout:linePoint];
-
- CGPoint popoverPoint = linePoint;
- if (_verticalForm) {
- popoverPoint.x = linePoint.x + _magnifierRangedOffset;
- } else {
- popoverPoint.y = linePoint.y + _magnifierRangedOffset;
- }
-
- CGPoint capturePoint;
- if (_verticalForm) {
- capturePoint.x = linePoint.x + kMagnifierRangedCaptureOffset;
- capturePoint.y = linePoint.y;
- } else {
- capturePoint.x = linePoint.x;
- capturePoint.y = linePoint.y + kMagnifierRangedCaptureOffset;
- }
-
- _magnifierRanged.hostPopoverCenter = popoverPoint;
- _magnifierRanged.hostCaptureCenter = capturePoint;
- if (!_state.showingMagnifierRanged) {
- _state.showingMagnifierRanged = YES;
- [[YYTextEffectWindow sharedWindow] showMagnifier:_magnifierRanged];
- } else {
- [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierRanged];
- }
- }
- }
- /// Update the showing magnifier.
- - (void)_updateMagnifier {
- if (YYTextIsAppExtension()) return;
-
- if (_state.showingMagnifierCaret) {
- [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierCaret];
- }
- if (_state.showingMagnifierRanged) {
- [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierRanged];
- }
- }
- /// Hide the `_magnifierCaret` and `_magnifierRanged`.
- - (void)_hideMagnifier {
- if (YYTextIsAppExtension()) return;
-
- if (_state.showingMagnifierCaret || _state.showingMagnifierRanged) {
- // disable touch began temporary to ignore caret animation overlap
- _state.ignoreTouchBegan = YES;
- __weak typeof(self) _self = self;
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- __strong typeof(_self) self = _self;
- if (self) self->_state.ignoreTouchBegan = NO;
- });
- }
-
- if (_state.showingMagnifierCaret) {
- _state.showingMagnifierCaret = NO;
- [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret];
- }
- if (_state.showingMagnifierRanged) {
- _state.showingMagnifierRanged = NO;
- [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged];
- }
- }
- /// Show and update the UIMenuController.
- - (void)_showMenu {
- CGRect rect;
- if (_selectionView.caretVisible) {
- rect = _selectionView.caretView.frame;
- } else if (_selectionView.selectionRects.count > 0) {
- YYTextSelectionRect *sRect = _selectionView.selectionRects.firstObject;
- rect = sRect.rect;
- for (NSUInteger i = 1; i < _selectionView.selectionRects.count; i++) {
- sRect = _selectionView.selectionRects[i];
- rect = CGRectUnion(rect, sRect.rect);
- }
-
- CGRect inter = CGRectIntersection(rect, self.bounds);
- if (!CGRectIsNull(inter) && inter.size.height > 1) {
- rect = inter; //clip to bounds
- } else {
- if (CGRectGetMinY(rect) < CGRectGetMinY(self.bounds)) {
- rect.size.height = 1;
- rect.origin.y = CGRectGetMinY(self.bounds);
- } else {
- rect.size.height = 1;
- rect.origin.y = CGRectGetMaxY(self.bounds);
- }
- }
-
- YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
- if (mgr.keyboardVisible) {
- CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
- CGRect kbInter = CGRectIntersection(rect, kbRect);
- if (!CGRectIsNull(kbInter) && kbInter.size.height > 1 && kbInter.size.width > 1) {
- // self is covered by keyboard
- if (CGRectGetMinY(kbInter) > CGRectGetMinY(rect)) { // keyboard at bottom
- rect.size.height -= kbInter.size.height;
- } else if (CGRectGetMaxY(kbInter) < CGRectGetMaxY(rect)) { // keyboard at top
- rect.origin.y += kbInter.size.height;
- rect.size.height -= kbInter.size.height;
- }
- }
- }
- } else {
- rect = _selectionView.bounds;
- }
-
- if (!self.isFirstResponder) {
- if (!_containerView.isFirstResponder) {
- [_containerView becomeFirstResponder];
- }
- }
-
- if (self.isFirstResponder || _containerView.isFirstResponder) {
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- UIMenuController *menu = [UIMenuController sharedMenuController];
- [menu setTargetRect:CGRectStandardize(rect) inView:_selectionView];
- [menu update];
- if (!_state.showingMenu || !menu.menuVisible) {
- _state.showingMenu = YES;
- [menu setMenuVisible:YES animated:YES];
- }
- });
- }
- }
- /// Hide the UIMenuController.
- - (void)_hideMenu {
- if (_state.showingMenu) {
- _state.showingMenu = NO;
- UIMenuController *menu = [UIMenuController sharedMenuController];
- [menu setMenuVisible:NO animated:YES];
- }
- if (_containerView.isFirstResponder) {
- _state.ignoreFirstResponder = YES;
- [_containerView resignFirstResponder]; // it will call [self becomeFirstResponder], ignore it temporary.
- _state.ignoreFirstResponder = NO;
- }
- }
- /// Show highlight layout based on `_highlight` and `_highlightRange`
- /// If the `_highlightLayout` is nil, try to create.
- - (void)_showHighlightAnimated:(BOOL)animated {
- NSTimeInterval fadeDuration = animated ? kHighlightFadeDuration : 0;
- if (!_highlight) return;
- if (!_highlightLayout) {
- NSMutableAttributedString *hiText = (_delectedText ? _delectedText : _innerText).mutableCopy;
- NSDictionary *newAttrs = _highlight.attributes;
- [newAttrs enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
- [hiText yy_setAttribute:key value:value range:_highlightRange];
- }];
- _highlightLayout = [YYTextLayout layoutWithContainer:_innerContainer text:hiText];
- if (!_highlightLayout) _highlight = nil;
- }
-
- if (_highlightLayout && !_state.showingHighlight) {
- _state.showingHighlight = YES;
- [_containerView setLayout:_highlightLayout withFadeDuration:fadeDuration];
- }
- }
- /// Show `_innerLayout` instead of `_highlightLayout`.
- /// It does not destory the `_highlightLayout`.
- - (void)_hideHighlightAnimated:(BOOL)animated {
- NSTimeInterval fadeDuration = animated ? kHighlightFadeDuration : 0;
- if (_state.showingHighlight) {
- _state.showingHighlight = NO;
- [_containerView setLayout:_innerLayout withFadeDuration:fadeDuration];
- }
- }
- /// Show `_innerLayout` and destory the `_highlight` and `_highlightLayout`.
- - (void)_removeHighlightAnimated:(BOOL)animated {
- [self _hideHighlightAnimated:animated];
- _highlight = nil;
- _highlightLayout = nil;
- }
- /// Scroll current selected range to visible.
- - (void)_scrollSelectedRangeToVisible {
- [self _scrollRangeToVisible:_selectedTextRange];
- }
- /// Scroll range to visible, take account into keyboard and insets.
- - (void)_scrollRangeToVisible:(YYTextRange *)range {
- if (!range) return;
- CGRect rect = [_innerLayout rectForRange:range];
- if (CGRectIsNull(rect)) return;
- rect = [self _convertRectFromLayout:rect];
- rect = [_containerView convertRect:rect toView:self];
-
- if (rect.size.width < 1) rect.size.width = 1;
- if (rect.size.height < 1) rect.size.height = 1;
- CGFloat extend = 3;
-
- BOOL insetModified = NO;
- YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
-
- if (mgr.keyboardVisible && self.window && self.superview && self.isFirstResponder && !_verticalForm) {
- CGRect bounds = self.bounds;
- bounds.origin = CGPointZero;
- CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
- kbRect.origin.y -= _extraAccessoryViewHeight;
- kbRect.size.height += _extraAccessoryViewHeight;
-
- kbRect.origin.x -= self.contentOffset.x;
- kbRect.origin.y -= self.contentOffset.y;
- CGRect inter = CGRectIntersection(bounds, kbRect);
- if (!CGRectIsNull(inter) && inter.size.height > 1 && inter.size.width > extend) { // self is covered by keyboard
- if (CGRectGetMinY(inter) > CGRectGetMinY(bounds)) { // keyboard below self.top
-
- UIEdgeInsets originalContentInset = self.contentInset;
- UIEdgeInsets originalScrollIndicatorInsets = self.scrollIndicatorInsets;
- if (_insetModifiedByKeyboard) {
- originalContentInset = _originalContentInset;
- originalScrollIndicatorInsets = _originalScrollIndicatorInsets;
- }
-
- if (originalContentInset.bottom < inter.size.height + extend) {
- insetModified = YES;
- if (!_insetModifiedByKeyboard) {
- _insetModifiedByKeyboard = YES;
- _originalContentInset = self.contentInset;
- _originalScrollIndicatorInsets = self.scrollIndicatorInsets;
- }
- UIEdgeInsets newInset = originalContentInset;
- UIEdgeInsets newIndicatorInsets = originalScrollIndicatorInsets;
- newInset.bottom = inter.size.height + extend;
- newIndicatorInsets.bottom = newInset.bottom;
- UIViewAnimationOptions curve;
- if (kiOS7Later) {
- curve = 7 << 16;
- } else {
- curve = UIViewAnimationOptionCurveEaseInOut;
- }
- [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | curve animations:^{
- [super setContentInset:newInset];
- [super setScrollIndicatorInsets:newIndicatorInsets];
- [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO];
- } completion:NULL];
- }
- }
- }
- }
- if (!insetModified) {
- [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut animations:^{
- [self _restoreInsetsAnimated:NO];
- [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO];
- } completion:NULL];
- }
- }
- /// Restore contents insets if modified by keyboard.
- - (void)_restoreInsetsAnimated:(BOOL)animated {
- if (_insetModifiedByKeyboard) {
- _insetModifiedByKeyboard = NO;
- if (animated) {
- [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut animations:^{
- [super setContentInset:_originalContentInset];
- [super setScrollIndicatorInsets:_originalScrollIndicatorInsets];
- } completion:NULL];
- } else {
- [super setContentInset:_originalContentInset];
- [super setScrollIndicatorInsets:_originalScrollIndicatorInsets];
- }
- }
- }
- /// Keyboard frame changed, scroll the caret to visible range, or modify the content insets.
- - (void)_keyboardChanged {
- if (!self.isFirstResponder) return;
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- if ([YYTextKeyboardManager defaultManager].keyboardVisible) {
- [self _scrollRangeToVisible:_selectedTextRange];
- } else {
- [self _restoreInsetsAnimated:YES];
- }
- [self _updateMagnifier];
- if (_state.showingMenu) {
- [self _showMenu];
- }
- });
- }
- /// Start long press timer, used for 'highlight' range text action.
- - (void)_startLongPressTimer {
- [_longPressTimer invalidate];
- _longPressTimer = [NSTimer timerWithTimeInterval:kLongPressMinimumDuration
- target:[YYTextWeakProxy proxyWithTarget:self]
- selector:@selector(_trackDidLongPress)
- userInfo:nil
- repeats:NO];
- [[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes];
- }
- /// Invalidate the long press timer.
- - (void)_endLongPressTimer {
- [_longPressTimer invalidate];
- _longPressTimer = nil;
- }
- /// Long press detected.
- - (void)_trackDidLongPress {
- [self _endLongPressTimer];
-
- BOOL dealLongPressAction = NO;
- if (_state.showingHighlight) {
- [self _hideMenu];
-
- if (_highlight.longPressAction) {
- dealLongPressAction = YES;
- CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
- rect = [self _convertRectFromLayout:rect];
- _highlight.longPressAction(self, _innerText, _highlightRange, rect);
- [self _endTouchTracking];
- } else {
- BOOL shouldHighlight = YES;
- if ([self.delegate respondsToSelector:@selector(textView:shouldLongPressHighlight:inRange:)]) {
- shouldHighlight = [self.delegate textView:self shouldLongPressHighlight:_highlight inRange:_highlightRange];
- }
- if (shouldHighlight && [self.delegate respondsToSelector:@selector(textView:didLongPressHighlight:inRange:rect:)]) {
- dealLongPressAction = YES;
- CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
- rect = [self _convertRectFromLayout:rect];
- [self.delegate textView:self didLongPressHighlight:_highlight inRange:_highlightRange rect:rect];
- [self _endTouchTracking];
- }
- }
- }
-
- if (!dealLongPressAction){
- [self _removeHighlightAnimated:NO];
- if (_state.trackingTouch) {
- if (_state.trackingGrabber) {
- self.panGestureRecognizer.enabled = NO;
- [self _hideMenu];
- [self _showMagnifierRanged];
- } else if (self.isFirstResponder){
- self.panGestureRecognizer.enabled = NO;
- _selectionView.caretBlinks = NO;
- _state.trackingCaret = YES;
- CGPoint trackingPoint = [self _convertPointToLayout:_trackingPoint];
- YYTextPosition *newPos = [_innerLayout closestPositionToPoint:trackingPoint];
- newPos = [self _correctedTextPosition:newPos];
- if (newPos) {
- if (_markedTextRange) {
- if ([newPos compare:_markedTextRange.start] != NSOrderedDescending) {
- newPos = _markedTextRange.start;
- } else if ([newPos compare:_markedTextRange.end] != NSOrderedAscending) {
- newPos = _markedTextRange.end;
- }
- }
- _trackingRange = [YYTextRange rangeWithRange:NSMakeRange(newPos.offset, 0) affinity:newPos.affinity];
- [self _updateSelectionView];
- }
- [self _hideMenu];
-
- if (_markedTextRange) {
- [self _showMagnifierRanged];
- } else {
- [self _showMagnifierCaret];
- }
- } else if (self.selectable) {
- self.panGestureRecognizer.enabled = NO;
- _state.trackingPreSelect = YES;
- _state.selectedWithoutEdit = NO;
- [self _updateTextRangeByTrackingPreSelect];
- [self _updateSelectionView];
- [self _showMagnifierCaret];
- }
- }
- }
- }
- /// Start auto scroll timer, used for auto scroll tick.
- - (void)_startAutoScrollTimer {
- if (!_autoScrollTimer) {
- [_autoScrollTimer invalidate];
- _autoScrollTimer = [NSTimer timerWithTimeInterval:kAutoScrollMinimumDuration
- target:[YYTextWeakProxy proxyWithTarget:self]
- selector:@selector(_trackDidTickAutoScroll)
- userInfo:nil
- repeats:YES];
- [[NSRunLoop currentRunLoop] addTimer:_autoScrollTimer forMode:NSRunLoopCommonModes];
- }
- }
- /// Invalidate the auto scroll, and restore the text view state.
- - (void)_endAutoScrollTimer {
- if (_state.autoScrollTicked) [self flashScrollIndicators];
- [_autoScrollTimer invalidate];
- _autoScrollTimer = nil;
- _autoScrollOffset = 0;
- _autoScrollAcceleration = 0;
- _state.autoScrollTicked = NO;
-
- if (_magnifierCaret.captureDisabled) {
- _magnifierCaret.captureDisabled = NO;
- if (_state.showingMagnifierCaret) {
- [self _showMagnifierCaret];
- }
- }
- if (_magnifierRanged.captureDisabled) {
- _magnifierRanged.captureDisabled = NO;
- if (_state.showingMagnifierRanged) {
- [self _showMagnifierRanged];
- }
- }
- }
- /// Auto scroll ticked by timer.
- - (void)_trackDidTickAutoScroll {
- if (_autoScrollOffset != 0) {
- _magnifierCaret.captureDisabled = YES;
- _magnifierRanged.captureDisabled = YES;
-
- CGPoint offset = self.contentOffset;
- if (_verticalForm) {
- offset.x += _autoScrollOffset;
-
- if (_autoScrollAcceleration > 0) {
- offset.x += ((_autoScrollOffset > 0 ? 1 : -1) * _autoScrollAcceleration * _autoScrollAcceleration * 0.5);
- }
- _autoScrollAcceleration++;
- offset.x = round(offset.x);
- if (_autoScrollOffset < 0) {
- if (offset.x < -self.contentInset.left) offset.x = -self.contentInset.left;
- } else {
- CGFloat maxOffsetX = self.contentSize.width - self.bounds.size.width + self.contentInset.right;
- if (offset.x > maxOffsetX) offset.x = maxOffsetX;
- }
- if (offset.x < -self.contentInset.left) offset.x = -self.contentInset.left;
- } else {
- offset.y += _autoScrollOffset;
- if (_autoScrollAcceleration > 0) {
- offset.y += ((_autoScrollOffset > 0 ? 1 : -1) * _autoScrollAcceleration * _autoScrollAcceleration * 0.5);
- }
- _autoScrollAcceleration++;
- offset.y = round(offset.y);
- if (_autoScrollOffset < 0) {
- if (offset.y < -self.contentInset.top) offset.y = -self.contentInset.top;
- } else {
- CGFloat maxOffsetY = self.contentSize.height - self.bounds.size.height + self.contentInset.bottom;
- if (offset.y > maxOffsetY) offset.y = maxOffsetY;
- }
- if (offset.y < -self.contentInset.top) offset.y = -self.contentInset.top;
- }
-
- BOOL shouldScroll;
- if (_verticalForm) {
- shouldScroll = fabs(offset.x -self.contentOffset.x) > 0.5;
- } else {
- shouldScroll = fabs(offset.y -self.contentOffset.y) > 0.5;
- }
-
- if (shouldScroll) {
- _state.autoScrollTicked = YES;
- _trackingPoint.x += offset.x - self.contentOffset.x;
- _trackingPoint.y += offset.y - self.contentOffset.y;
- [UIView animateWithDuration:kAutoScrollMinimumDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear animations:^{
- [self setContentOffset:offset];
- } completion:^(BOOL finished) {
- if (_state.trackingTouch) {
- if (_state.trackingGrabber) {
- [self _showMagnifierRanged];
- [self _updateTextRangeByTrackingGrabber];
- } else if (_state.trackingPreSelect) {
- [self _showMagnifierCaret];
- [self _updateTextRangeByTrackingPreSelect];
- } else if (_state.trackingCaret) {
- if (_markedTextRange) {
- [self _showMagnifierRanged];
- } else {
- [self _showMagnifierCaret];
- }
- [self _updateTextRangeByTrackingCaret];
- }
- [self _updateSelectionView];
- }
- }];
- } else {
- [self _endAutoScrollTimer];
- }
- } else {
- [self _endAutoScrollTimer];
- }
- }
- /// End current touch tracking (if is tracking now), and update the state.
- - (void)_endTouchTracking {
- if (!_state.trackingTouch) return;
-
- _state.trackingTouch = NO;
- _state.trackingGrabber = NO;
- _state.trackingCaret = NO;
- _state.trackingPreSelect = NO;
- _state.touchMoved = NO;
- _state.deleteConfirm = NO;
- _state.clearsOnInsertionOnce = NO;
- _trackingRange = nil;
- _selectionView.caretBlinks = YES;
-
- [self _removeHighlightAnimated:YES];
- [self _hideMagnifier];
- [self _endLongPressTimer];
- [self _endAutoScrollTimer];
- [self _updateSelectionView];
-
- self.panGestureRecognizer.enabled = self.scrollEnabled;
- }
- /// Start a timer to fix the selection dot.
- - (void)_startSelectionDotFixTimer {
- [_selectionDotFixTimer invalidate];
- _longPressTimer = [NSTimer timerWithTimeInterval:1/15.0
- target:[YYTextWeakProxy proxyWithTarget:self]
- selector:@selector(_fixSelectionDot)
- userInfo:nil
- repeats:NO];
- [[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes];
- }
- /// End the timer.
- - (void)_endSelectionDotFixTimer {
- [_selectionDotFixTimer invalidate];
- _selectionDotFixTimer = nil;
- }
- /// If it shows selection grabber and this view was moved by super view,
- /// update the selection dot in window.
- - (void)_fixSelectionDot {
- if (YYTextIsAppExtension()) return;
- CGPoint origin = [self yy_convertPoint:CGPointZero toViewOrWindow:[YYTextEffectWindow sharedWindow]];
- if (!CGPointEqualToPoint(origin, _previousOriginInWindow)) {
- _previousOriginInWindow = origin;
- [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
- [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
- }
- }
- /// Try to get the character range/position with word granularity from the tokenizer.
- - (YYTextRange *)_getClosestTokenRangeAtPosition:(YYTextPosition *)position {
- position = [self _correctedTextPosition:position];
- if (!position) return nil;
- YYTextRange *range = nil;
- if (_tokenizer) {
- range = (id)[_tokenizer rangeEnclosingPosition:position withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward];
- if (range.asRange.length == 0) {
- range = (id)[_tokenizer rangeEnclosingPosition:position withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward];
- }
- }
-
- if (!range || range.asRange.length == 0) {
- range = [_innerLayout textRangeByExtendingPosition:position inDirection:UITextLayoutDirectionRight offset:1];
- range = [self _correctedTextRange:range];
- if (range.asRange.length == 0) {
- range = [_innerLayout textRangeByExtendingPosition:position inDirection:UITextLayoutDirectionLeft offset:1];
- range = [self _correctedTextRange:range];
- }
- } else {
- YYTextRange *extStart = [_innerLayout textRangeByExtendingPosition:range.start];
- YYTextRange *extEnd = [_innerLayout textRangeByExtendingPosition:range.end];
- if (extStart && extEnd) {
- NSArray *arr = [@[extStart.start, extStart.end, extEnd.start, extEnd.end] sortedArrayUsingSelector:@selector(compare:)];
- range = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject];
- }
- }
-
- range = [self _correctedTextRange:range];
- if (range.asRange.length == 0) {
- range = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
- }
-
- return [self _correctedTextRange:range];
- }
- /// Try to get the character range/position with word granularity from the tokenizer.
- - (YYTextRange *)_getClosestTokenRangeAtPoint:(CGPoint)point {
- point = [self _convertPointToLayout:point];
- YYTextRange *touchRange = [_innerLayout closestTextRangeAtPoint:point];
- touchRange = [self _correctedTextRange:touchRange];
-
- if (_tokenizer && touchRange) {
- YYTextRange *encEnd = (id)[_tokenizer rangeEnclosingPosition:touchRange.end withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward];
- YYTextRange *encStart = (id)[_tokenizer rangeEnclosingPosition:touchRange.start withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward];
- if (encEnd && encStart) {
- NSArray *arr = [@[encEnd.start, encEnd.end, encStart.start, encStart.end] sortedArrayUsingSelector:@selector(compare:)];
- touchRange = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject];
- }
- }
-
- if (touchRange) {
- YYTextRange *extStart = [_innerLayout textRangeByExtendingPosition:touchRange.start];
- YYTextRange *extEnd = [_innerLayout textRangeByExtendingPosition:touchRange.end];
- if (extStart && extEnd) {
- NSArray *arr = [@[extStart.start, extStart.end, extEnd.start, extEnd.end] sortedArrayUsingSelector:@selector(compare:)];
- touchRange = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject];
- }
- }
-
- if (!touchRange) touchRange = [YYTextRange defaultRange];
-
- if (_innerText.length && touchRange.asRange.length == 0) {
- touchRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
- }
-
- return touchRange;
- }
- /// Try to get the highlight property. If exist, the range will be returnd by the range pointer.
- /// If the delegate ignore the highlight, returns nil.
- - (YYTextHighlight *)_getHighlightAtPoint:(CGPoint)point range:(NSRangePointer)range {
- if (!_highlightable || !_innerLayout.containsHighlight) return nil;
- point = [self _convertPointToLayout:point];
- YYTextRange *textRange = [_innerLayout textRangeAtPoint:point];
- textRange = [self _correctedTextRange:textRange];
- if (!textRange) return nil;
- NSUInteger startIndex = textRange.start.offset;
- if (startIndex == _innerText.length) {
- if (startIndex == 0) return nil;
- else startIndex--;
- }
- NSRange highlightRange = {0};
- NSAttributedString *text = _delectedText ? _delectedText : _innerText;
- YYTextHighlight *highlight = [text attribute:YYTextHighlightAttributeName
- atIndex:startIndex
- longestEffectiveRange:&highlightRange
- inRange:NSMakeRange(0, _innerText.length)];
-
- if (!highlight) return nil;
-
- BOOL shouldTap = YES, shouldLongPress = YES;
- if (!highlight.tapAction && !highlight.longPressAction) {
- if ([self.delegate respondsToSelector:@selector(textView:shouldTapHighlight:inRange:)]) {
- shouldTap = [self.delegate textView:self shouldTapHighlight:highlight inRange:highlightRange];
- }
- if ([self.delegate respondsToSelector:@selector(textView:shouldLongPressHighlight:inRange:)]) {
- shouldLongPress = [self.delegate textView:self shouldLongPressHighlight:highlight inRange:highlightRange];
- }
- }
- if (!shouldTap && !shouldLongPress) return nil;
- if (range) *range = highlightRange;
- return highlight;
- }
- /// Return the ranged magnifier popover offset from the baseline, base on `_trackingPoint`.
- - (CGFloat)_getMagnifierRangedOffset {
- CGPoint magPoint = _trackingPoint;
- magPoint = [self _convertPointToLayout:magPoint];
- if (_verticalForm) {
- magPoint.x += kMagnifierRangedTrackFix;
- } else {
- magPoint.y += kMagnifierRangedTrackFix;
- }
- YYTextPosition *position = [_innerLayout closestPositionToPoint:magPoint];
- NSUInteger lineIndex = [_innerLayout lineIndexForPosition:position];
- if (lineIndex < _innerLayout.lines.count) {
- YYTextLine *line = _innerLayout.lines[lineIndex];
- if (_verticalForm) {
- magPoint.x = YYTEXT_CLAMP(magPoint.x, line.left, line.right);
- return magPoint.x - line.position.x + kMagnifierRangedPopoverOffset;
- } else {
- magPoint.y = YYTEXT_CLAMP(magPoint.y, line.top, line.bottom);
- return magPoint.y - line.position.y + kMagnifierRangedPopoverOffset;
- }
- } else {
- return 0;
- }
- }
- /// Return a YYTextMoveDirection from `_touchBeganPoint` to `_trackingPoint`.
- - (unsigned int)_getMoveDirection {
- CGFloat moveH = _trackingPoint.x - _touchBeganPoint.x;
- CGFloat moveV = _trackingPoint.y - _touchBeganPoint.y;
- if (fabs(moveH) > fabs(moveV)) {
- if (fabs(moveH) > kLongPressAllowableMovement) {
- return moveH > 0 ? kRight : kLeft;
- }
- } else {
- if (fabs(moveV) > kLongPressAllowableMovement) {
- return moveV > 0 ? kBottom : kTop;
- }
- }
- return 0;
- }
- /// Get the auto scroll offset in one tick time.
- - (CGFloat)_getAutoscrollOffset {
- if (!_state.trackingTouch) return 0;
-
- CGRect bounds = self.bounds;
- bounds.origin = CGPointZero;
- YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
- if (mgr.keyboardVisible && self.window && self.superview && self.isFirstResponder && !_verticalForm) {
- CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
- kbRect.origin.y -= _extraAccessoryViewHeight;
- kbRect.size.height += _extraAccessoryViewHeight;
-
- kbRect.origin.x -= self.contentOffset.x;
- kbRect.origin.y -= self.contentOffset.y;
- CGRect inter = CGRectIntersection(bounds, kbRect);
- if (!CGRectIsNull(inter) && inter.size.height > 1 && inter.size.width > 1) {
- if (CGRectGetMinY(inter) > CGRectGetMinY(bounds)) {
- bounds.size.height -= inter.size.height;
- }
- }
- }
-
- CGPoint point = _trackingPoint;
- point.x -= self.contentOffset.x;
- point.y -= self.contentOffset.y;
-
- CGFloat maxOfs = 32; // a good value ~
- CGFloat ofs = 0;
- if (_verticalForm) {
- if (point.x < self.contentInset.left) {
- ofs = (point.x - self.contentInset.left - 5) * 0.5;
- if (ofs < -maxOfs) ofs = -maxOfs;
- } else if (point.x > bounds.size.width) {
- ofs = ((point.x - bounds.size.width) + 5) * 0.5;
- if (ofs > maxOfs) ofs = maxOfs;
- }
- } else {
- if (point.y < self.contentInset.top) {
- ofs = (point.y - self.contentInset.top - 5) * 0.5;
- if (ofs < -maxOfs) ofs = -maxOfs;
- } else if (point.y > bounds.size.height) {
- ofs = ((point.y - bounds.size.height) + 5) * 0.5;
- if (ofs > maxOfs) ofs = maxOfs;
- }
- }
- return ofs;
- }
- /// Visible size based on bounds and insets
- - (CGSize)_getVisibleSize {
- CGSize visibleSize = self.bounds.size;
- visibleSize.width -= self.contentInset.left - self.contentInset.right;
- visibleSize.height -= self.contentInset.top - self.contentInset.bottom;
- if (visibleSize.width < 0) visibleSize.width = 0;
- if (visibleSize.height < 0) visibleSize.height = 0;
- return visibleSize;
- }
- /// Returns whether the text view can paste data from pastboard.
- - (BOOL)_isPasteboardContainsValidValue {
- UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
- if (pasteboard.string.length > 0) {
- return YES;
- }
- if (pasteboard.yy_AttributedString.length > 0) {
- if (_allowsPasteAttributedString) {
- return YES;
- }
- }
- if (pasteboard.image || pasteboard.yy_ImageData.length > 0) {
- if (_allowsPasteImage) {
- return YES;
- }
- }
- return NO;
- }
- /// Save current selected attributed text to pasteboard.
- - (void)_copySelectedTextToPasteboard {
- if (_allowsCopyAttributedString) {
- NSAttributedString *text = [_innerText attributedSubstringFromRange:_selectedTextRange.asRange];
- if (text.length) {
- [UIPasteboard generalPasteboard].yy_AttributedString = text;
- }
- } else {
- NSString *string = [_innerText yy_plainTextForRange:_selectedTextRange.asRange];
- if (string.length) {
- [UIPasteboard generalPasteboard].string = string;
- }
- }
- }
- /// Update the text view state when pasteboard changed.
- - (void)_pasteboardChanged {
- if (_state.showingMenu) {
- UIMenuController *menu = [UIMenuController sharedMenuController];
- [menu update];
- }
- }
- /// Whether the position is valid (not out of bounds).
- - (BOOL)_isTextPositionValid:(YYTextPosition *)position {
- if (!position) return NO;
- if (position.offset < 0) return NO;
- if (position.offset > _innerText.length) return NO;
- if (position.offset == 0 && position.affinity == YYTextAffinityBackward) return NO;
- if (position.offset == _innerText.length && position.affinity == YYTextAffinityBackward) return NO;
- return YES;
- }
- /// Whether the range is valid (not out of bounds).
- - (BOOL)_isTextRangeValid:(YYTextRange *)range {
- if (![self _isTextPositionValid:range.start]) return NO;
- if (![self _isTextPositionValid:range.end]) return NO;
- return YES;
- }
- /// Correct the position if it out of bounds.
- - (YYTextPosition *)_correctedTextPosition:(YYTextPosition *)position {
- if (!position) return nil;
- if ([self _isTextPositionValid:position]) return position;
- if (position.offset < 0) {
- return [YYTextPosition positionWithOffset:0];
- }
- if (position.offset > _innerText.length) {
- return [YYTextPosition positionWithOffset:_innerText.length];
- }
- if (position.offset == 0 && position.affinity == YYTextAffinityBackward) {
- return [YYTextPosition positionWithOffset:position.offset];
- }
- if (position.offset == _innerText.length && position.affinity == YYTextAffinityBackward) {
- return [YYTextPosition positionWithOffset:position.offset];
- }
- return position;
- }
- /// Correct the range if it out of bounds.
- - (YYTextRange *)_correctedTextRange:(YYTextRange *)range {
- if (!range) return nil;
- if ([self _isTextRangeValid:range]) return range;
- YYTextPosition *start = [self _correctedTextPosition:range.start];
- YYTextPosition *end = [self _correctedTextPosition:range.end];
- return [YYTextRange rangeWithStart:start end:end];
- }
- /// Convert the point from this view to text layout.
- - (CGPoint)_convertPointToLayout:(CGPoint)point {
- CGSize boundingSize = _innerLayout.textBoundingSize;
- if (_innerLayout.container.isVerticalForm) {
- CGFloat w = _innerLayout.textBoundingSize.width;
- if (w < self.bounds.size.width) w = self.bounds.size.width;
- point.x += _innerLayout.container.size.width - w;
- if (boundingSize.width < self.bounds.size.width) {
- if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
- point.x += (self.bounds.size.width - boundingSize.width) * 0.5;
- } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
- point.x += (self.bounds.size.width - boundingSize.width);
- }
- }
- return point;
- } else {
- if (boundingSize.height < self.bounds.size.height) {
- if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
- point.y -= (self.bounds.size.height - boundingSize.height) * 0.5;
- } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
- point.y -= (self.bounds.size.height - boundingSize.height);
- }
- }
- return point;
- }
- }
- /// Convert the point from text layout to this view.
- - (CGPoint)_convertPointFromLayout:(CGPoint)point {
- CGSize boundingSize = _innerLayout.textBoundingSize;
- if (_innerLayout.container.isVerticalForm) {
- CGFloat w = _innerLayout.textBoundingSize.width;
- if (w < self.bounds.size.width) w = self.bounds.size.width;
- point.x -= _innerLayout.container.size.width - w;
- if (boundingSize.width < self.bounds.size.width) {
- if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
- point.x -= (self.bounds.size.width - boundingSize.width) * 0.5;
- } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
- point.x -= (self.bounds.size.width - boundingSize.width);
- }
- }
- return point;
- } else {
- if (boundingSize.height < self.bounds.size.height) {
- if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
- point.y += (self.bounds.size.height - boundingSize.height) * 0.5;
- } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
- point.y += (self.bounds.size.height - boundingSize.height);
- }
- }
- return point;
- }
- }
- /// Convert the rect from this view to text layout.
- - (CGRect)_convertRectToLayout:(CGRect)rect {
- rect.origin = [self _convertPointToLayout:rect.origin];
- return rect;
- }
- /// Convert the rect from text layout to this view.
- - (CGRect)_convertRectFromLayout:(CGRect)rect {
- rect.origin = [self _convertPointFromLayout:rect.origin];
- return rect;
- }
- /// Replace the range with the text, and change the `_selectTextRange`.
- /// The caller should make sure the `range` and `text` are valid before call this method.
- - (void)_replaceRange:(YYTextRange *)range withText:(NSString *)text notifyToDelegate:(BOOL)notify{
- if (NSEqualRanges(range.asRange, _selectedTextRange.asRange)) {
- if (notify) [_inputDelegate selectionWillChange:self];
- NSRange newRange = NSMakeRange(0, 0);
- newRange.location = _selectedTextRange.start.offset + text.length;
- _selectedTextRange = [YYTextRange rangeWithRange:newRange];
- if (notify) [_inputDelegate selectionDidChange:self];
- } else {
- if (range.asRange.length != text.length) {
- if (notify) [_inputDelegate selectionWillChange:self];
- NSRange unionRange = NSIntersectionRange(_selectedTextRange.asRange, range.asRange);
- if (unionRange.length == 0) {
- // no intersection
- if (range.end.offset <= _selectedTextRange.start.offset) {
- NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length;
- NSRange newRange = _selectedTextRange.asRange;
- newRange.location += ofs;
- _selectedTextRange = [YYTextRange rangeWithRange:newRange];
- }
- } else if (unionRange.length == _selectedTextRange.asRange.length) {
- // target range contains selected range
- _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(range.start.offset + text.length, 0)];
- } else if (range.start.offset >= _selectedTextRange.start.offset &&
- range.end.offset <= _selectedTextRange.end.offset) {
- // target range inside selected range
- NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length;
- NSRange newRange = _selectedTextRange.asRange;
- newRange.length += ofs;
- _selectedTextRange = [YYTextRange rangeWithRange:newRange];
- } else {
- // interleaving
- if (range.start.offset < _selectedTextRange.start.offset) {
- NSRange newRange = _selectedTextRange.asRange;
- newRange.location = range.start.offset + text.length;
- newRange.length -= unionRange.length;
- _selectedTextRange = [YYTextRange rangeWithRange:newRange];
- } else {
- NSRange newRange = _selectedTextRange.asRange;
- newRange.length -= unionRange.length;
- _selectedTextRange = [YYTextRange rangeWithRange:newRange];
- }
- }
- _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
- if (notify) [_inputDelegate selectionDidChange:self];
- }
- }
- if (notify) [_inputDelegate textWillChange:self];
- NSRange newRange = NSMakeRange(range.asRange.location, text.length);
- [_innerText replaceCharactersInRange:range.asRange withString:text];
- [_innerText yy_removeDiscontinuousAttributesInRange:newRange];
- if (notify) [_inputDelegate textDidChange:self];
- }
- /// Save current typing attributes to the attributes holder.
- - (void)_updateAttributesHolder {
- if (_innerText.length > 0) {
- NSUInteger index = _selectedTextRange.end.offset == 0 ? 0 : _selectedTextRange.end.offset - 1;
- NSDictionary *attributes = [_innerText yy_attributesAtIndex:index];
- if (!attributes) attributes = @{};
- _typingAttributesHolder.yy_attributes = attributes;
- [_typingAttributesHolder yy_removeDiscontinuousAttributesInRange:NSMakeRange(0, _typingAttributesHolder.length)];
- [_typingAttributesHolder removeAttribute:YYTextBorderAttributeName range:NSMakeRange(0, _typingAttributesHolder.length)];
- [_typingAttributesHolder removeAttribute:YYTextBackgroundBorderAttributeName range:NSMakeRange(0, _typingAttributesHolder.length)];
- }
- }
- /// Update outer properties from current inner data.
- - (void)_updateOuterProperties {
- [self _updateAttributesHolder];
- NSParagraphStyle *style = _innerText.yy_paragraphStyle;
- if (!style) style = _typingAttributesHolder.yy_paragraphStyle;
- if (!style) style = [NSParagraphStyle defaultParagraphStyle];
-
- UIFont *font = _innerText.yy_font;
- if (!font) font = _typingAttributesHolder.yy_font;
- if (!font) font = [self _defaultFont];
-
- UIColor *color = _innerText.yy_color;
- if (!color) color = _typingAttributesHolder.yy_color;
- if (!color) color = [UIColor blackColor];
-
- [self _setText:[_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)]];
- [self _setFont:font];
- [self _setTextColor:color];
- [self _setTextAlignment:style.alignment];
- [self _setSelectedRange:_selectedTextRange.asRange];
- [self _setTypingAttributes:_typingAttributesHolder.yy_attributes];
- [self _setAttributedText:_innerText];
- }
- /// Parse text with `textParser` and update the _selectedTextRange.
- /// @return Whether changed (text or selection)
- - (BOOL)_parseText {
- if (self.textParser) {
- YYTextRange *oldTextRange = _selectedTextRange;
- NSRange newRange = _selectedTextRange.asRange;
-
- [_inputDelegate textWillChange:self];
- BOOL textChanged = [self.textParser parseText:_innerText selectedRange:&newRange];
- [_inputDelegate textDidChange:self];
-
- YYTextRange *newTextRange = [YYTextRange rangeWithRange:newRange];
- newTextRange = [self _correctedTextRange:newTextRange];
-
- if (![oldTextRange isEqual:newTextRange]) {
- [_inputDelegate selectionWillChange:self];
- _selectedTextRange = newTextRange;
- [_inputDelegate selectionDidChange:self];
- }
- return textChanged;
- }
- return NO;
- }
- /// Returns whether the text should be detected by the data detector.
- - (BOOL)_shouldDetectText {
- if (!_dataDetector) return NO;
- if (!_highlightable) return NO;
- if (_linkTextAttributes.count == 0 && _highlightTextAttributes.count == 0) return NO;
- if (self.isFirstResponder || _containerView.isFirstResponder) return NO;
- return YES;
- }
- /// Detect the data in text and add highlight to the data range.
- /// @return Whether detected.
- - (BOOL)_detectText:(NSMutableAttributedString *)text {
- if (![self _shouldDetectText]) return NO;
- if (text.length == 0) return NO;
- __block BOOL detected = NO;
- [_dataDetector enumerateMatchesInString:text.string options:kNilOptions range:NSMakeRange(0, text.length) usingBlock: ^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
- switch (result.resultType) {
- case NSTextCheckingTypeDate:
- case NSTextCheckingTypeAddress:
- case NSTextCheckingTypeLink:
- case NSTextCheckingTypePhoneNumber: {
- detected = YES;
- if (_highlightTextAttributes.count) {
- YYTextHighlight *highlight = [YYTextHighlight highlightWithAttributes:_highlightTextAttributes];
- [text yy_setTextHighlight:highlight range:result.range];
- }
- if (_linkTextAttributes.count) {
- [_linkTextAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
- [text yy_setAttribute:key value:obj range:result.range];
- }];
- }
- } break;
- default:
- break;
- }
- }];
- return detected;
- }
- /// Returns the `root` view controller (returns nil if not found).
- - (UIViewController *)_getRootViewController {
- UIViewController *ctrl = nil;
- UIApplication *app = YYTextSharedApplication();
- if (!ctrl) ctrl = app.keyWindow.rootViewController;
- if (!ctrl) ctrl = [app.windows.firstObject rootViewController];
- if (!ctrl) ctrl = self.yy_viewController;
- if (!ctrl) return nil;
-
- while (!ctrl.view.window && ctrl.presentedViewController) {
- ctrl = ctrl.presentedViewController;
- }
- if (!ctrl.view.window) return nil;
- return ctrl;
- }
- /// Clear the undo and redo stack, and capture current state to undo stack.
- - (void)_resetUndoAndRedoStack {
- [_undoStack removeAllObjects];
- [_redoStack removeAllObjects];
- _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange];
- _lastTypeRange = _selectedTextRange.asRange;
- [_undoStack addObject:object];
- }
- /// Clear the redo stack.
- - (void)_resetRedoStack {
- [_redoStack removeAllObjects];
- }
- /// Capture current state to undo stack.
- - (void)_saveToUndoStack {
- if (!_allowsUndoAndRedo) return;
- _YYTextViewUndoObject *lastObject = _undoStack.lastObject;
- if ([lastObject.text isEqualToAttributedString:self.attributedText]) return;
-
- _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange];
- _lastTypeRange = _selectedTextRange.asRange;
- [_undoStack addObject:object];
- while (_undoStack.count > _maximumUndoLevel) {
- [_undoStack removeObjectAtIndex:0];
- }
- }
- /// Capture current state to redo stack.
- - (void)_saveToRedoStack {
- if (!_allowsUndoAndRedo) return;
- _YYTextViewUndoObject *lastObject = _redoStack.lastObject;
- if ([lastObject.text isEqualToAttributedString:self.attributedText]) return;
-
- _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange];
- [_redoStack addObject:object];
- while (_redoStack.count > _maximumUndoLevel) {
- [_redoStack removeObjectAtIndex:0];
- }
- }
- - (BOOL)_canUndo {
- if (_undoStack.count == 0) return NO;
- _YYTextViewUndoObject *object = _undoStack.lastObject;
- if ([object.text isEqualToAttributedString:_innerText]) return NO;
- return YES;
- }
- - (BOOL)_canRedo {
- if (_redoStack.count == 0) return NO;
- _YYTextViewUndoObject *object = _undoStack.lastObject;
- if ([object.text isEqualToAttributedString:_innerText]) return NO;
- return YES;
- }
- - (void)_undo {
- if (![self _canUndo]) return;
- [self _saveToRedoStack];
- _YYTextViewUndoObject *object = _undoStack.lastObject;
- [_undoStack removeLastObject];
-
- _state.insideUndoBlock = YES;
- self.attributedText = object.text;
- self.selectedRange = object.selectedRange;
- _state.insideUndoBlock = NO;
- }
- - (void)_redo {
- if (![self _canRedo]) return;
- [self _saveToUndoStack];
- _YYTextViewUndoObject *object = _redoStack.lastObject;
- [_redoStack removeLastObject];
-
- _state.insideUndoBlock = YES;
- self.attributedText = object.text;
- self.selectedRange = object.selectedRange;
- _state.insideUndoBlock = NO;
- }
- - (void)_restoreFirstResponderAfterUndoAlert {
- if (_state.firstResponderBeforeUndoAlert) {
- [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0];
- }
- }
- /// Show undo alert if it can undo or redo.
- #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
- - (void)_showUndoRedoAlert NS_EXTENSION_UNAVAILABLE_IOS(""){
- _state.firstResponderBeforeUndoAlert = self.isFirstResponder;
- __weak typeof(self) _self = self;
- NSArray *strings = [self _localizedUndoStrings];
- BOOL canUndo = [self _canUndo];
- BOOL canRedo = [self _canRedo];
-
- UIViewController *ctrl = [self _getRootViewController];
-
- if (canUndo && canRedo) {
- if (kiOS8Later) {
- UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[4] message:@"" preferredStyle:UIAlertControllerStyleAlert];
- [alert addAction:[UIAlertAction actionWithTitle:strings[3] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
- [_self _undo];
- [_self _restoreFirstResponderAfterUndoAlert];
- }]];
- [alert addAction:[UIAlertAction actionWithTitle:strings[2] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
- [_self _redo];
- [_self _restoreFirstResponderAfterUndoAlert];
- }]];
- [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
- [_self _restoreFirstResponderAfterUndoAlert];
- }]];
- [ctrl presentViewController:alert animated:YES completion:nil];
- } else {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[4] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[3], strings[2], nil];
- [alert show];
- #pragma clang diagnostic pop
- }
- } else if (canUndo) {
- if (kiOS8Later) {
- UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[4] message:@"" preferredStyle:UIAlertControllerStyleAlert];
- [alert addAction:[UIAlertAction actionWithTitle:strings[3] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
- [_self _undo];
- [_self _restoreFirstResponderAfterUndoAlert];
- }]];
- [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
- [_self _restoreFirstResponderAfterUndoAlert];
- }]];
- [ctrl presentViewController:alert animated:YES completion:nil];
- } else {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[4] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[3], nil];
- [alert show];
- #pragma clang diagnostic pop
- }
- } else if (canRedo) {
- if (kiOS8Later) {
- UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[2] message:@"" preferredStyle:UIAlertControllerStyleAlert];
- [alert addAction:[UIAlertAction actionWithTitle:strings[1] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
- [_self _redo];
- [_self _restoreFirstResponderAfterUndoAlert];
- }]];
- [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
- [_self _restoreFirstResponderAfterUndoAlert];
- }]];
- [ctrl presentViewController:alert animated:YES completion:nil];
- } else {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[2] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[1], nil];
- [alert show];
- #pragma clang diagnostic pop
- }
- }
- }
- #endif
- /// Get the localized undo alert strings based on app's main bundle.
- - (NSArray *)_localizedUndoStrings {
- static NSArray *strings = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- NSDictionary *dic = @{
- @"ar" : @[ @"إلغاء", @"إعادة", @"إعادة الكتابة", @"تراجع", @"تراجع عن الكتابة" ],
- @"ca" : @[ @"Cancel·lar", @"Refer", @"Refer l’escriptura", @"Desfer", @"Desfer l’escriptura" ],
- @"cs" : @[ @"Zrušit", @"Opakovat akci", @"Opakovat akci Psát", @"Odvolat akci", @"Odvolat akci Psát" ],
- @"da" : @[ @"Annuller", @"Gentag", @"Gentag Indtastning", @"Fortryd", @"Fortryd Indtastning" ],
- @"de" : @[ @"Abbrechen", @"Wiederholen", @"Eingabe wiederholen", @"Widerrufen", @"Eingabe widerrufen" ],
- @"el" : @[ @"Ακύρωση", @"Επανάληψη", @"Επανάληψη πληκτρολόγησης", @"Αναίρεση", @"Αναίρεση πληκτρολόγησης" ],
- @"en" : @[ @"Cancel", @"Redo", @"Redo Typing", @"Undo", @"Undo Typing" ],
- @"es" : @[ @"Cancelar", @"Rehacer", @"Rehacer escritura", @"Deshacer", @"Deshacer escritura" ],
- @"es_MX" : @[ @"Cancelar", @"Rehacer", @"Rehacer escritura", @"Deshacer", @"Deshacer escritura" ],
- @"fi" : @[ @"Kumoa", @"Tee sittenkin", @"Kirjoita sittenkin", @"Peru", @"Peru kirjoitus" ],
- @"fr" : @[ @"Annuler", @"Rétablir", @"Rétablir la saisie", @"Annuler", @"Annuler la saisie" ],
- @"he" : @[ @"ביטול", @"חזור על הפעולה האחרונה", @"חזור על הקלדה", @"בטל", @"בטל הקלדה" ],
- @"hr" : @[ @"Odustani", @"Ponovi", @"Ponovno upiši", @"Poništi", @"Poništi upisivanje" ],
- @"hu" : @[ @"Mégsem", @"Ismétlés", @"Gépelés ismétlése", @"Visszavonás", @"Gépelés visszavonása" ],
- @"id" : @[ @"Batalkan", @"Ulang", @"Ulang Pengetikan", @"Kembalikan", @"Batalkan Pengetikan" ],
- @"it" : @[ @"Annulla", @"Ripristina originale", @"Ripristina Inserimento", @"Annulla", @"Annulla Inserimento" ],
- @"ja" : @[ @"キャンセル", @"やり直す", @"やり直す - 入力", @"取り消す", @"取り消す - 入力" ],
- @"ko" : @[ @"취소", @"실행 복귀", @"입력 복귀", @"실행 취소", @"입력 실행 취소" ],
- @"ms" : @[ @"Batal", @"Buat semula", @"Ulang Penaipan", @"Buat asal", @"Buat asal Penaipan" ],
- @"nb" : @[ @"Avbryt", @"Utfør likevel", @"Utfør skriving likevel", @"Angre", @"Angre skriving" ],
- @"nl" : @[ @"Annuleer", @"Opnieuw", @"Opnieuw typen", @"Herstel", @"Herstel typen" ],
- @"pl" : @[ @"Anuluj", @"Przywróć", @"Przywróć Wpisz", @"Cofnij", @"Cofnij Wpisz" ],
- @"pt" : @[ @"Cancelar", @"Refazer", @"Refazer Digitação", @"Desfazer", @"Desfazer Digitação" ],
- @"pt_PT" : @[ @"Cancelar", @"Refazer", @"Refazer digitar", @"Desfazer", @"Desfazer digitar" ],
- @"ro" : @[ @"Renunță", @"Refă", @"Refă tastare", @"Anulează", @"Anulează tastare" ],
- @"ru" : @[ @"Отменить", @"Повторить", @"Повторить набор на клавиатуре", @"Отменить", @"Отменить набор на клавиатуре" ],
- @"sk" : @[ @"Zrušiť", @"Obnoviť", @"Obnoviť písanie", @"Odvolať", @"Odvolať písanie" ],
- @"sv" : @[ @"Avbryt", @"Gör om", @"Gör om skriven text", @"Ångra", @"Ångra skriven text" ],
- @"th" : @[ @"ยกเลิก", @"ทำกลับมาใหม่", @"ป้อนกลับมาใหม่", @"เลิกทำ", @"เลิกป้อน" ],
- @"tr" : @[ @"Vazgeç", @"Yinele", @"Yazmayı Yinele", @"Geri Al", @"Yazmayı Geri Al" ],
- @"uk" : @[ @"Скасувати", @"Повторити", @"Повторити введення", @"Відмінити", @"Відмінити введення" ],
- @"vi" : @[ @"Hủy", @"Làm lại", @"Làm lại thao tác Nhập", @"Hoàn tác", @"Hoàn tác thao tác Nhập" ],
- @"zh" : @[ @"取消", @"重做", @"重做键入", @"撤销", @"撤销键入" ],
- @"zh_CN" : @[ @"取消", @"重做", @"重做键入", @"撤销", @"撤销键入" ],
- @"zh_HK" : @[ @"取消", @"重做", @"重做輸入", @"還原", @"還原輸入" ],
- @"zh_TW" : @[ @"取消", @"重做", @"重做輸入", @"還原", @"還原輸入" ]
- };
- NSString *preferred = [[NSBundle mainBundle] preferredLocalizations].firstObject;
- if (preferred.length == 0) preferred = @"English";
- NSString *canonical = [NSLocale canonicalLocaleIdentifierFromString:preferred];
- if (canonical.length == 0) canonical = @"en";
- strings = dic[canonical];
- if (!strings && ([canonical rangeOfString:@"_"].location != NSNotFound)) {
- NSString *prefix = [canonical componentsSeparatedByString:@"_"].firstObject;
- if (prefix.length) strings = dic[prefix];
- }
- if (!strings) strings = dic[@"en"];
- });
- return strings;
- }
- /// Returns the default font for text view (same as CoreText).
- - (UIFont *)_defaultFont {
- return [UIFont systemFontOfSize:12];
- }
- /// Returns the default tint color for text view (used for caret and select range background).
- - (UIColor *)_defaultTintColor {
- return [UIColor colorWithRed:69/255.0 green:111/255.0 blue:238/255.0 alpha:1];
- }
- /// Returns the default placeholder color for text view (same as UITextField).
- - (UIColor *)_defaultPlaceholderColor {
- return [UIColor colorWithRed:0 green:0 blue:25/255.0 alpha:44/255.0];
- }
- #pragma mark - Private Setter
- - (void)_setText:(NSString *)text {
- if (_text == text || [_text isEqualToString:text]) return;
- [self willChangeValueForKey:@"text"];
- _text = text.copy;
- if (!_text) _text = @"";
- [self didChangeValueForKey:@"text"];
- self.accessibilityLabel = _text;
- }
- - (void)_setFont:(UIFont *)font {
- if (_font == font || [_font isEqual:font]) return;
- [self willChangeValueForKey:@"font"];
- _font = font;
- [self didChangeValueForKey:@"font"];
- }
- - (void)_setTextColor:(UIColor *)textColor {
- if (_textColor == textColor) return;
- if (_textColor && textColor) {
- if (CFGetTypeID(_textColor.CGColor) == CFGetTypeID(textColor.CGColor) &&
- CFGetTypeID(_textColor.CGColor) == CGColorGetTypeID()) {
- if ([_textColor isEqual:textColor]) {
- return;
- }
- }
- }
- [self willChangeValueForKey:@"textColor"];
- _textColor = textColor;
- [self didChangeValueForKey:@"textColor"];
- }
- - (void)_setTextAlignment:(NSTextAlignment)textAlignment {
- if (_textAlignment == textAlignment) return;
- [self willChangeValueForKey:@"textAlignment"];
- _textAlignment = textAlignment;
- [self didChangeValueForKey:@"textAlignment"];
- }
- - (void)_setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes {
- if (_dataDetectorTypes == dataDetectorTypes) return;
- [self willChangeValueForKey:@"dataDetectorTypes"];
- _dataDetectorTypes = dataDetectorTypes;
- [self didChangeValueForKey:@"dataDetectorTypes"];
- }
- - (void)_setLinkTextAttributes:(NSDictionary *)linkTextAttributes {
- if (_linkTextAttributes == linkTextAttributes || [_linkTextAttributes isEqual:linkTextAttributes]) return;
- [self willChangeValueForKey:@"linkTextAttributes"];
- _linkTextAttributes = linkTextAttributes.copy;
- [self didChangeValueForKey:@"linkTextAttributes"];
- }
- - (void)_setHighlightTextAttributes:(NSDictionary *)highlightTextAttributes {
- if (_highlightTextAttributes == highlightTextAttributes || [_highlightTextAttributes isEqual:highlightTextAttributes]) return;
- [self willChangeValueForKey:@"highlightTextAttributes"];
- _highlightTextAttributes = highlightTextAttributes.copy;
- [self didChangeValueForKey:@"highlightTextAttributes"];
- }
- - (void)_setTextParser:(id<YYTextParser>)textParser {
- if (_textParser == textParser || [_textParser isEqual:textParser]) return;
- [self willChangeValueForKey:@"textParser"];
- _textParser = textParser;
- [self didChangeValueForKey:@"textParser"];
- }
- - (void)_setAttributedText:(NSAttributedString *)attributedText {
- if (_attributedText == attributedText || [_attributedText isEqual:attributedText]) return;
- [self willChangeValueForKey:@"attributedText"];
- _attributedText = attributedText.copy;
- if (!_attributedText) _attributedText = [NSAttributedString new];
- [self didChangeValueForKey:@"attributedText"];
- }
- - (void)_setTextContainerInset:(UIEdgeInsets)textContainerInset {
- if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return;
- [self willChangeValueForKey:@"textContainerInset"];
- _textContainerInset = textContainerInset;
- [self didChangeValueForKey:@"textContainerInset"];
- }
- - (void)_setExclusionPaths:(NSArray *)exclusionPaths {
- if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return;
- [self willChangeValueForKey:@"exclusionPaths"];
- _exclusionPaths = exclusionPaths.copy;
- [self didChangeValueForKey:@"exclusionPaths"];
- }
- - (void)_setVerticalForm:(BOOL)verticalForm {
- if (_verticalForm == verticalForm) return;
- [self willChangeValueForKey:@"verticalForm"];
- _verticalForm = verticalForm;
- [self didChangeValueForKey:@"verticalForm"];
- }
- - (void)_setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier {
- if (_linePositionModifier == linePositionModifier) return;
- [self willChangeValueForKey:@"linePositionModifier"];
- _linePositionModifier = [(NSObject *)linePositionModifier copy];
- [self didChangeValueForKey:@"linePositionModifier"];
- }
- - (void)_setSelectedRange:(NSRange)selectedRange {
- if (NSEqualRanges(_selectedRange, selectedRange)) return;
- [self willChangeValueForKey:@"selectedRange"];
- _selectedRange = selectedRange;
- [self didChangeValueForKey:@"selectedRange"];
- if ([self.delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) {
- [self.delegate textViewDidChangeSelection:self];
- }
- }
- - (void)_setTypingAttributes:(NSDictionary *)typingAttributes {
- if (_typingAttributes == typingAttributes || [_typingAttributes isEqual:typingAttributes]) return;
- [self willChangeValueForKey:@"typingAttributes"];
- _typingAttributes = typingAttributes.copy;
- [self didChangeValueForKey:@"typingAttributes"];
- }
- #pragma mark - Private Init
- - (void)_initTextView {
- self.delaysContentTouches = NO;
- self.canCancelContentTouches = YES;
- self.multipleTouchEnabled = NO;
- self.clipsToBounds = YES;
- [super setDelegate:self];
-
- _text = @"";
- _attributedText = [NSAttributedString new];
-
- // UITextInputTraits
- _autocapitalizationType = UITextAutocapitalizationTypeSentences;
- _autocorrectionType = UITextAutocorrectionTypeDefault;
- _spellCheckingType = UITextSpellCheckingTypeDefault;
- _keyboardType = UIKeyboardTypeDefault;
- _keyboardAppearance = UIKeyboardAppearanceDefault;
- _returnKeyType = UIReturnKeyDefault;
- _enablesReturnKeyAutomatically = NO;
- _secureTextEntry = NO;
-
- // UITextInput
- _selectedTextRange = [YYTextRange defaultRange];
- _markedTextRange = nil;
- _markedTextStyle = nil;
- _tokenizer = [[UITextInputStringTokenizer alloc] initWithTextInput:self];
-
- _editable = YES;
- _selectable = YES;
- _highlightable = YES;
- _allowsCopyAttributedString = YES;
- _textAlignment = NSTextAlignmentNatural;
-
- _innerText = [NSMutableAttributedString new];
- _innerContainer = [YYTextContainer new];
- _innerContainer.insets = kDefaultInset;
- _textContainerInset = kDefaultInset;
- _typingAttributesHolder = [[NSMutableAttributedString alloc] initWithString:@" "];
- _linkTextAttributes = @{NSForegroundColorAttributeName : [self _defaultTintColor],
- (id)kCTForegroundColorAttributeName : (id)[self _defaultTintColor].CGColor};
-
- YYTextHighlight *highlight = [YYTextHighlight new];
- YYTextBorder * border = [YYTextBorder new];
- border.insets = UIEdgeInsetsMake(-2, -2, -2, -2);
- border.fillColor = [UIColor colorWithWhite:0.1 alpha:0.2];
- border.cornerRadius = 3;
- [highlight setBorder:border];
- _highlightTextAttributes = highlight.attributes.copy;
-
- _placeHolderView = [UIImageView new];
- _placeHolderView.userInteractionEnabled = NO;
- _placeHolderView.hidden = YES;
-
- _containerView = [YYTextContainerView new];
- _containerView.hostView = self;
-
- _selectionView = [YYTextSelectionView new];
- _selectionView.userInteractionEnabled = NO;
- _selectionView.hostView = self;
- _selectionView.color = [self _defaultTintColor];
-
- _magnifierCaret = [YYTextMagnifier magnifierWithType:YYTextMagnifierTypeCaret];
- _magnifierCaret.hostView = _containerView;
- _magnifierRanged = [YYTextMagnifier magnifierWithType:YYTextMagnifierTypeRanged];
- _magnifierRanged.hostView = _containerView;
-
- [self addSubview:_placeHolderView];
- [self addSubview:_containerView];
- [self addSubview:_selectionView];
-
- _undoStack = [NSMutableArray new];
- _redoStack = [NSMutableArray new];
- _allowsUndoAndRedo = YES;
- _maximumUndoLevel = kDefaultUndoLevelMax;
-
- self.debugOption = [YYTextDebugOption sharedDebugOption];
- [YYTextDebugOption addDebugTarget:self];
-
- [self _updateInnerContainerSize];
- [self _update];
-
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_pasteboardChanged) name:UIPasteboardChangedNotification object:nil];
- [[YYTextKeyboardManager defaultManager] addObserver:self];
-
- self.isAccessibilityElement = YES;
- }
- #pragma mark - Public
- - (instancetype)initWithFrame:(CGRect)frame {
- self = [super initWithFrame:frame];
- if (!self) return nil;
- [self _initTextView];
- return self;
- }
- - (void)dealloc {
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIPasteboardChangedNotification object:nil];
- [[YYTextKeyboardManager defaultManager] removeObserver:self];
-
- [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
- [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret];
- [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged];
-
- [YYTextDebugOption removeDebugTarget:self];
-
- [_longPressTimer invalidate];
- [_autoScrollTimer invalidate];
- [_selectionDotFixTimer invalidate];
- }
- - (void)scrollRangeToVisible:(NSRange)range {
- YYTextRange *textRange = [YYTextRange rangeWithRange:range];
- textRange = [self _correctedTextRange:textRange];
- [self _scrollRangeToVisible:textRange];
- }
- #pragma mark - Property
- - (void)setText:(NSString *)text {
- if (_text == text || [_text isEqualToString:text]) return;
- [self _setText:text];
-
- _state.selectedWithoutEdit = NO;
- _state.deleteConfirm = NO;
- [self _endTouchTracking];
- [self _hideMenu];
- [self _resetUndoAndRedoStack];
- [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)] withText:text];
- }
- - (void)setFont:(UIFont *)font {
- if (_font == font || [_font isEqual:font]) return;
- [self _setFont:font];
-
- _state.typingAttributesOnce = NO;
- _typingAttributesHolder.yy_font = font;
- _innerText.yy_font = font;
- [self _resetUndoAndRedoStack];
- [self _commitUpdate];
- }
- - (void)setTextColor:(UIColor *)textColor {
- if (_textColor == textColor || [_textColor isEqual:textColor]) return;
- [self _setTextColor:textColor];
-
- _state.typingAttributesOnce = NO;
- _typingAttributesHolder.yy_color = textColor;
- _innerText.yy_color = textColor;
- [self _resetUndoAndRedoStack];
- [self _commitUpdate];
- }
- - (void)setTextAlignment:(NSTextAlignment)textAlignment {
- if (_textAlignment == textAlignment) return;
- [self _setTextAlignment:textAlignment];
-
- _typingAttributesHolder.yy_alignment = textAlignment;
- _innerText.yy_alignment = textAlignment;
- [self _resetUndoAndRedoStack];
- [self _commitUpdate];
- }
- - (void)setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes {
- if (_dataDetectorTypes == dataDetectorTypes) return;
- [self _setDataDetectorTypes:dataDetectorTypes];
- NSTextCheckingType type = YYTextNSTextCheckingTypeFromUIDataDetectorType(dataDetectorTypes);
- _dataDetector = type ? [NSDataDetector dataDetectorWithTypes:type error:NULL] : nil;
- [self _resetUndoAndRedoStack];
- [self _commitUpdate];
- }
- - (void)setLinkTextAttributes:(NSDictionary *)linkTextAttributes {
- if (_linkTextAttributes == linkTextAttributes || [_linkTextAttributes isEqual:linkTextAttributes]) return;
- [self _setLinkTextAttributes:linkTextAttributes];
- if (_dataDetector) {
- [self _commitUpdate];
- }
- }
- - (void)setHighlightTextAttributes:(NSDictionary *)highlightTextAttributes {
- if (_highlightTextAttributes == highlightTextAttributes || [_highlightTextAttributes isEqual:highlightTextAttributes]) return;
- [self _setHighlightTextAttributes:highlightTextAttributes];
- if (_dataDetector) {
- [self _commitUpdate];
- }
- }
- - (void)setTextParser:(id<YYTextParser>)textParser {
- if (_textParser == textParser || [_textParser isEqual:textParser]) return;
- [self _setTextParser:textParser];
- if (textParser && _text.length) {
- [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _text.length)] withText:_text];
- }
- [self _resetUndoAndRedoStack];
- [self _commitUpdate];
- }
- - (void)setTypingAttributes:(NSDictionary *)typingAttributes {
- [self _setTypingAttributes:typingAttributes];
- _state.typingAttributesOnce = YES;
- [typingAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
- [_typingAttributesHolder yy_setAttribute:key value:obj];
- }];
- [self _commitUpdate];
- }
- - (void)setAttributedText:(NSAttributedString *)attributedText {
- if (_attributedText == attributedText) return;
- [self _setAttributedText:attributedText];
- _state.typingAttributesOnce = NO;
-
- NSMutableAttributedString *text = attributedText.mutableCopy;
- if (text.length == 0) {
- [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)] withText:@""];
- return;
- }
- if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
- BOOL should = [self.delegate textView:self shouldChangeTextInRange:NSMakeRange(0, _innerText.length) replacementText:text.string];
- if (!should) return;
- }
-
- _state.selectedWithoutEdit = NO;
- _state.deleteConfirm = NO;
- [self _endTouchTracking];
- [self _hideMenu];
-
- [_inputDelegate selectionWillChange:self];
- [_inputDelegate textWillChange:self];
- _innerText = text;
- [self _parseText];
- _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
- [_inputDelegate textDidChange:self];
- [_inputDelegate selectionDidChange:self];
-
- [self _setAttributedText:text];
- if (_innerText.length > 0) {
- _typingAttributesHolder.yy_attributes = [_innerText yy_attributesAtIndex:_innerText.length - 1];
- }
-
- [self _updateOuterProperties];
- [self _updateLayout];
- [self _updateSelectionView];
-
- if (self.isFirstResponder) {
- [self _scrollRangeToVisible:_selectedTextRange];
- }
-
- if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
- [self.delegate textViewDidChange:self];
- }
- [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self];
-
- if (!_state.insideUndoBlock) {
- [self _resetUndoAndRedoStack];
- }
- }
- - (void)setTextVerticalAlignment:(YYTextVerticalAlignment)textVerticalAlignment {
- if (_textVerticalAlignment == textVerticalAlignment) return;
- [self willChangeValueForKey:@"textVerticalAlignment"];
- _textVerticalAlignment = textVerticalAlignment;
- [self didChangeValueForKey:@"textVerticalAlignment"];
- _containerView.textVerticalAlignment = textVerticalAlignment;
- [self _commitUpdate];
- }
- - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset {
- if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return;
- [self _setTextContainerInset:textContainerInset];
- _innerContainer.insets = textContainerInset;
- [self _commitUpdate];
- }
- - (void)setExclusionPaths:(NSArray *)exclusionPaths {
- if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return;
- [self _setExclusionPaths:exclusionPaths];
- _innerContainer.exclusionPaths = exclusionPaths;
- if (_innerContainer.isVerticalForm) {
- CGAffineTransform trans = CGAffineTransformMakeTranslation(_innerContainer.size.width - self.bounds.size.width, 0);
- [_innerContainer.exclusionPaths enumerateObjectsUsingBlock:^(UIBezierPath *path, NSUInteger idx, BOOL *stop) {
- [path applyTransform:trans];
- }];
- }
- [self _commitUpdate];
- }
- - (void)setVerticalForm:(BOOL)verticalForm {
- if (_verticalForm == verticalForm) return;
- [self _setVerticalForm:verticalForm];
- _innerContainer.verticalForm = verticalForm;
- _selectionView.verticalForm = verticalForm;
-
- [self _updateInnerContainerSize];
-
- if (verticalForm) {
- if (UIEdgeInsetsEqualToEdgeInsets(_innerContainer.insets, kDefaultInset)) {
- _innerContainer.insets = kDefaultVerticalInset;
- [self _setTextContainerInset:kDefaultVerticalInset];
- }
- } else {
- if (UIEdgeInsetsEqualToEdgeInsets(_innerContainer.insets, kDefaultVerticalInset)) {
- _innerContainer.insets = kDefaultInset;
- [self _setTextContainerInset:kDefaultInset];
- }
- }
-
- _innerContainer.exclusionPaths = _exclusionPaths;
- if (verticalForm) {
- CGAffineTransform trans = CGAffineTransformMakeTranslation(_innerContainer.size.width - self.bounds.size.width, 0);
- [_innerContainer.exclusionPaths enumerateObjectsUsingBlock:^(UIBezierPath *path, NSUInteger idx, BOOL *stop) {
- [path applyTransform:trans];
- }];
- }
-
- [self _keyboardChanged];
- [self _commitUpdate];
- }
- - (void)setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier {
- if (_linePositionModifier == linePositionModifier) return;
- [self _setLinePositionModifier:linePositionModifier];
- _innerContainer.linePositionModifier = linePositionModifier;
- [self _commitUpdate];
- }
- - (void)setSelectedRange:(NSRange)selectedRange {
- if (NSEqualRanges(_selectedRange, selectedRange)) return;
- if (_markedTextRange) return;
- _state.typingAttributesOnce = NO;
-
- YYTextRange *range = [YYTextRange rangeWithRange:selectedRange];
- range = [self _correctedTextRange:range];
- [self _endTouchTracking];
- _selectedTextRange = range;
- [self _updateSelectionView];
-
- [self _setSelectedRange:range.asRange];
-
- if (!_state.insideUndoBlock) {
- [self _resetUndoAndRedoStack];
- }
- }
- - (void)setHighlightable:(BOOL)highlightable {
- if (_highlightable == highlightable) return;
- [self willChangeValueForKey:@"highlightable"];
- _highlightable = highlightable;
- [self didChangeValueForKey:@"highlightable"];
- [self _commitUpdate];
- }
- - (void)setEditable:(BOOL)editable {
- if (_editable == editable) return;
- [self willChangeValueForKey:@"editable"];
- _editable = editable;
- [self didChangeValueForKey:@"editable"];
- if (!editable) {
- [self resignFirstResponder];
- }
- }
- - (void)setSelectable:(BOOL)selectable {
- if (_selectable == selectable) return;
- [self willChangeValueForKey:@"selectable"];
- _selectable = selectable;
- [self didChangeValueForKey:@"selectable"];
- if (!selectable) {
- if (self.isFirstResponder) {
- [self resignFirstResponder];
- } else {
- _state.selectedWithoutEdit = NO;
- [self _endTouchTracking];
- [self _hideMenu];
- [self _updateSelectionView];
- }
- }
- }
- - (void)setClearsOnInsertion:(BOOL)clearsOnInsertion {
- if (_clearsOnInsertion == clearsOnInsertion) return;
- _clearsOnInsertion = clearsOnInsertion;
- if (clearsOnInsertion) {
- if (self.isFirstResponder) {
- self.selectedRange = NSMakeRange(0, _attributedText.length);
- } else {
- _state.clearsOnInsertionOnce = YES;
- }
- }
- }
- - (void)setDebugOption:(YYTextDebugOption *)debugOption {
- _containerView.debugOption = debugOption;
- }
- - (YYTextDebugOption *)debugOption {
- return _containerView.debugOption;
- }
- - (YYTextLayout *)textLayout {
- [self _updateIfNeeded];
- return _innerLayout;
- }
- - (void)setPlaceholderText:(NSString *)placeholderText {
- if (_placeholderAttributedText.length > 0) {
- if (placeholderText.length > 0) {
- [((NSMutableAttributedString *)_placeholderAttributedText) replaceCharactersInRange:NSMakeRange(0, _placeholderAttributedText.length) withString:placeholderText];
- } else {
- [((NSMutableAttributedString *)_placeholderAttributedText) replaceCharactersInRange:NSMakeRange(0, _placeholderAttributedText.length) withString:@""];
- }
- ((NSMutableAttributedString *)_placeholderAttributedText).yy_font = _placeholderFont;
- ((NSMutableAttributedString *)_placeholderAttributedText).yy_color = _placeholderTextColor;
- } else {
- if (placeholderText.length > 0) {
- NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:placeholderText];
- if (!_placeholderFont) _placeholderFont = _font;
- if (!_placeholderFont) _placeholderFont = [self _defaultFont];
- if (!_placeholderTextColor) _placeholderTextColor = [self _defaultPlaceholderColor];
- atr.yy_font = _placeholderFont;
- atr.yy_color = _placeholderTextColor;
- _placeholderAttributedText = atr;
- }
- }
- _placeholderText = [_placeholderAttributedText yy_plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)];
- [self _commitPlaceholderUpdate];
- }
- - (void)setPlaceholderFont:(UIFont *)placeholderFont {
- _placeholderFont = placeholderFont;
- ((NSMutableAttributedString *)_placeholderAttributedText).yy_font = _placeholderFont;
- [self _commitPlaceholderUpdate];
- }
- - (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor {
- _placeholderTextColor = placeholderTextColor;
- ((NSMutableAttributedString *)_placeholderAttributedText).yy_color = _placeholderTextColor;
- [self _commitPlaceholderUpdate];
- }
- - (void)setPlaceholderAttributedText:(NSAttributedString *)placeholderAttributedText {
- _placeholderAttributedText = placeholderAttributedText.mutableCopy;
- _placeholderText = [_placeholderAttributedText yy_plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)];
- _placeholderFont = _placeholderAttributedText.yy_font;
- _placeholderTextColor = _placeholderAttributedText.yy_color;
- [self _commitPlaceholderUpdate];
- }
- #pragma mark - Override For Protect
- - (void)setMultipleTouchEnabled:(BOOL)multipleTouchEnabled {
- [super setMultipleTouchEnabled:NO]; // must not enabled
- }
- - (void)setContentInset:(UIEdgeInsets)contentInset {
- UIEdgeInsets oldInsets = self.contentInset;
- if (_insetModifiedByKeyboard) {
- _originalContentInset = contentInset;
- } else {
- [super setContentInset:contentInset];
- BOOL changed = !UIEdgeInsetsEqualToEdgeInsets(oldInsets, contentInset);
- if (changed) {
- [self _updateInnerContainerSize];
- [self _commitUpdate];
- [self _commitPlaceholderUpdate];
- }
- }
- }
- - (void)setScrollIndicatorInsets:(UIEdgeInsets)scrollIndicatorInsets {
- if (_insetModifiedByKeyboard) {
- _originalScrollIndicatorInsets = scrollIndicatorInsets;
- } else {
- [super setScrollIndicatorInsets:scrollIndicatorInsets];
- }
- }
- - (void)setFrame:(CGRect)frame {
- CGSize oldSize = self.bounds.size;
- [super setFrame:frame];
- CGSize newSize = self.bounds.size;
- BOOL changed = _innerContainer.isVerticalForm ? (oldSize.height != newSize.height) : (oldSize.width != newSize.width);
- if (changed) {
- [self _updateInnerContainerSize];
- [self _commitUpdate];
- }
- if (!CGSizeEqualToSize(oldSize, newSize)) {
- [self _commitPlaceholderUpdate];
- }
- }
- - (void)setBounds:(CGRect)bounds {
- CGSize oldSize = self.bounds.size;
- [super setBounds:bounds];
- CGSize newSize = self.bounds.size;
- BOOL changed = _innerContainer.isVerticalForm ? (oldSize.height != newSize.height) : (oldSize.width != newSize.width);
- if (changed) {
- [self _updateInnerContainerSize];
- [self _commitUpdate];
- }
- if (!CGSizeEqualToSize(oldSize, newSize)) {
- [self _commitPlaceholderUpdate];
- }
- }
- - (void)tintColorDidChange {
- if ([self respondsToSelector:@selector(tintColor)]) {
- UIColor *color = self.tintColor;
- NSMutableDictionary *attrs = _highlightTextAttributes.mutableCopy;
- NSMutableDictionary *linkAttrs = _linkTextAttributes.mutableCopy;
- if (!linkAttrs) linkAttrs = @{}.mutableCopy;
- if (!color) {
- [attrs removeObjectForKey:NSForegroundColorAttributeName];
- [attrs removeObjectForKey:(id)kCTForegroundColorAttributeName];
- [linkAttrs setObject:[self _defaultTintColor] forKey:NSForegroundColorAttributeName];
- [linkAttrs setObject:(id)[self _defaultTintColor].CGColor forKey:(id)kCTForegroundColorAttributeName];
- } else {
- [attrs setObject:color forKey:NSForegroundColorAttributeName];
- [attrs setObject:(id)color.CGColor forKey:(id)kCTForegroundColorAttributeName];
- [linkAttrs setObject:color forKey:NSForegroundColorAttributeName];
- [linkAttrs setObject:(id)color.CGColor forKey:(id)kCTForegroundColorAttributeName];
- }
- self.highlightTextAttributes = attrs;
- _selectionView.color = color ? color : [self _defaultTintColor];
- _linkTextAttributes = linkAttrs;
- [self _commitUpdate];
- }
- }
- - (CGSize)sizeThatFits:(CGSize)size {
- if (!_verticalForm && size.width <= 0) size.width = YYTextContainerMaxSize.width;
- if (_verticalForm && size.height <= 0) size.height = YYTextContainerMaxSize.height;
-
- if ((!_verticalForm && size.width == self.bounds.size.width) ||
- (_verticalForm && size.height == self.bounds.size.height)) {
- [self _updateIfNeeded];
- if (!_verticalForm) {
- if (_containerView.bounds.size.height <= size.height) {
- return _containerView.bounds.size;
- }
- } else {
- if (_containerView.bounds.size.width <= size.width) {
- return _containerView.bounds.size;
- }
- }
- }
-
- if (!_verticalForm) {
- size.height = YYTextContainerMaxSize.height;
- } else {
- size.width = YYTextContainerMaxSize.width;
- }
-
- YYTextContainer *container = [_innerContainer copy];
- container.size = size;
-
- YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
- return layout.textBoundingSize;
- }
- #pragma mark - Override UIResponder
- - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
- [self _updateIfNeeded];
- UITouch *touch = touches.anyObject;
- CGPoint point = [touch locationInView:_containerView];
-
- _touchBeganTime = _trackingTime = touch.timestamp;
- _touchBeganPoint = _trackingPoint = point;
- _trackingRange = _selectedTextRange;
-
- _state.trackingGrabber = NO;
- _state.trackingCaret = NO;
- _state.trackingPreSelect = NO;
- _state.trackingTouch = YES;
- _state.swallowTouch = YES;
- _state.touchMoved = NO;
-
- if (!self.isFirstResponder && !_state.selectedWithoutEdit && self.highlightable) {
- _highlight = [self _getHighlightAtPoint:point range:&_highlightRange];
- _highlightLayout = nil;
- }
-
- if ((!self.selectable && !_highlight) || _state.ignoreTouchBegan) {
- _state.swallowTouch = NO;
- _state.trackingTouch = NO;
- }
-
- if (_state.trackingTouch) {
- [self _startLongPressTimer];
- if (_highlight) {
- [self _showHighlightAnimated:NO];
- } else {
- if ([_selectionView isGrabberContainsPoint:point]) { // track grabber
- self.panGestureRecognizer.enabled = NO; // disable scroll view
- [self _hideMenu];
- _state.trackingGrabber = [_selectionView isStartGrabberContainsPoint:point] ? kStart : kEnd;
- _magnifierRangedOffset = [self _getMagnifierRangedOffset];
- } else {
- if (_selectedTextRange.asRange.length == 0 && self.isFirstResponder) {
- if ([_selectionView isCaretContainsPoint:point]) { // track caret
- _state.trackingCaret = YES;
- self.panGestureRecognizer.enabled = NO; // disable scroll view
- }
- }
- }
- }
- [self _updateSelectionView];
- }
-
- if (!_state.swallowTouch) [super touchesBegan:touches withEvent:event];
- }
- - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
- [self _updateIfNeeded];
- UITouch *touch = touches.anyObject;
- CGPoint point = [touch locationInView:_containerView];
-
- _trackingTime = touch.timestamp;
- _trackingPoint = point;
-
- if (!_state.touchMoved) {
- _state.touchMoved = [self _getMoveDirection];
- if (_state.touchMoved) [self _endLongPressTimer];
- }
- _state.clearsOnInsertionOnce = NO;
-
- if (_state.trackingTouch) {
- BOOL showMagnifierCaret = NO;
- BOOL showMagnifierRanged = NO;
-
- if (_highlight) {
-
- YYTextHighlight *highlight = [self _getHighlightAtPoint:_trackingPoint range:NULL];
- if (highlight == _highlight) {
- [self _showHighlightAnimated:YES];
- } else {
- [self _hideHighlightAnimated:YES];
- }
-
- } else {
- _trackingRange = _selectedTextRange;
- if (_state.trackingGrabber) {
- self.panGestureRecognizer.enabled = NO;
- [self _hideMenu];
- [self _updateTextRangeByTrackingGrabber];
- showMagnifierRanged = YES;
- } else if (_state.trackingPreSelect) {
- [self _updateTextRangeByTrackingPreSelect];
- showMagnifierCaret = YES;
- } else if (_state.trackingCaret || _markedTextRange || self.isFirstResponder) {
- if (_state.trackingCaret || _state.touchMoved) {
- _state.trackingCaret = YES;
- [self _hideMenu];
- if (_verticalForm) {
- if (_state.touchMoved == kTop || _state.touchMoved == kBottom) {
- self.panGestureRecognizer.enabled = NO;
- }
- } else {
- if (_state.touchMoved == kLeft || _state.touchMoved == kRight) {
- self.panGestureRecognizer.enabled = NO;
- }
- }
- [self _updateTextRangeByTrackingCaret];
- if (_markedTextRange) {
- showMagnifierRanged = YES;
- } else {
- showMagnifierCaret = YES;
- }
- }
- }
- }
- [self _updateSelectionView];
- if (showMagnifierCaret) [self _showMagnifierCaret];
- if (showMagnifierRanged) [self _showMagnifierRanged];
- }
-
- CGFloat autoScrollOffset = [self _getAutoscrollOffset];
- if (_autoScrollOffset != autoScrollOffset) {
- if (fabs(autoScrollOffset) < fabs(_autoScrollOffset)) {
- _autoScrollAcceleration *= 0.5;
- }
- _autoScrollOffset = autoScrollOffset;
- if (_autoScrollOffset != 0 && _state.touchMoved) {
- [self _startAutoScrollTimer];
- }
- }
-
- if (!_state.swallowTouch) [super touchesMoved:touches withEvent:event];
- }
- - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
- [self _updateIfNeeded];
-
- UITouch *touch = touches.anyObject;
- CGPoint point = [touch locationInView:_containerView];
-
- _trackingTime = touch.timestamp;
- _trackingPoint = point;
-
- if (!_state.touchMoved) {
- _state.touchMoved = [self _getMoveDirection];
- }
- if (_state.trackingTouch) {
- [self _hideMagnifier];
-
- if (_highlight) {
- if (_state.showingHighlight) {
- if (_highlight.tapAction) {
- CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
- rect = [self _convertRectFromLayout:rect];
- _highlight.tapAction(self, _innerText, _highlightRange, rect);
- } else {
- BOOL shouldTap = YES;
- if ([self.delegate respondsToSelector:@selector(textView:shouldTapHighlight:inRange:)]) {
- shouldTap = [self.delegate textView:self shouldTapHighlight:_highlight inRange:_highlightRange];
- }
- if (shouldTap && [self.delegate respondsToSelector:@selector(textView:didTapHighlight:inRange:rect:)]) {
- CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
- rect = [self _convertRectFromLayout:rect];
- [self.delegate textView:self didTapHighlight:_highlight inRange:_highlightRange rect:rect];
- }
- }
- [self _removeHighlightAnimated:YES];
- }
- } else {
- if (_state.trackingCaret) {
- if (_state.touchMoved) {
- [self _updateTextRangeByTrackingCaret];
- [self _showMenu];
- } else {
- if (_state.showingMenu) [self _hideMenu];
- else [self _showMenu];
- }
- } else if (_state.trackingGrabber) {
- [self _updateTextRangeByTrackingGrabber];
- [self _showMenu];
- } else if (_state.trackingPreSelect) {
- [self _updateTextRangeByTrackingPreSelect];
- if (_trackingRange.asRange.length > 0) {
- _state.selectedWithoutEdit = YES;
- [self _showMenu];
- } else {
- [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0];
- }
- } else if (_state.deleteConfirm || _markedTextRange) {
- [self _updateTextRangeByTrackingCaret];
- [self _hideMenu];
- } else {
- if (!_state.touchMoved) {
- if (_state.selectedWithoutEdit) {
- _state.selectedWithoutEdit = NO;
- [self _hideMenu];
- } else {
- if (self.isFirstResponder) {
- YYTextRange *_oldRange = _trackingRange;
- [self _updateTextRangeByTrackingCaret];
- if ([_oldRange isEqual:_trackingRange]) {
- if (_state.showingMenu) [self _hideMenu];
- else [self _showMenu];
- } else {
- [self _hideMenu];
- }
- } else {
- [self _hideMenu];
- if (_state.clearsOnInsertionOnce) {
- _state.clearsOnInsertionOnce = NO;
- _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
- [self _setSelectedRange:_selectedTextRange.asRange];
- } else {
- [self _updateTextRangeByTrackingCaret];
- }
- [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0];
- }
- }
- }
- }
- }
-
- if (_trackingRange && (![_trackingRange isEqual:_selectedTextRange] || _state.trackingPreSelect)) {
- if (![_trackingRange isEqual:_selectedTextRange]) {
- [_inputDelegate selectionWillChange:self];
- _selectedTextRange = _trackingRange;
- [_inputDelegate selectionDidChange:self];
- [self _updateAttributesHolder];
- [self _updateOuterProperties];
- }
- if (!_state.trackingGrabber && !_state.trackingPreSelect) {
- [self _scrollRangeToVisible:_selectedTextRange];
- }
- }
-
- [self _endTouchTracking];
- }
-
- if (!_state.swallowTouch) [super touchesEnded:touches withEvent:event];
- }
- - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
- [self _endTouchTracking];
- [self _hideMenu];
- if (!_state.swallowTouch) [super touchesCancelled:touches withEvent:event];
- }
- - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
- if (motion == UIEventSubtypeMotionShake && _allowsUndoAndRedo) {
- if (!YYTextIsAppExtension()) {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wundeclared-selector"
- [self performSelector:@selector(_showUndoRedoAlert)];
- #pragma clang diagnostic pop
- }
- } else {
- [super motionEnded:motion withEvent:event];
- }
- }
- - (BOOL)canBecomeFirstResponder {
- if (!self.isSelectable) return NO;
- if (!self.isEditable) return NO;
- if (_state.ignoreFirstResponder) return NO;
- if ([self.delegate respondsToSelector:@selector(textViewShouldBeginEditing:)]) {
- if (![self.delegate textViewShouldBeginEditing:self]) return NO;
- }
- return YES;
- }
- - (BOOL)becomeFirstResponder {
- BOOL isFirstResponder = self.isFirstResponder;
- if (isFirstResponder) return YES;
- BOOL shouldDetectData = [self _shouldDetectText];
- BOOL become = [super becomeFirstResponder];
- if (!isFirstResponder && become) {
- [self _endTouchTracking];
- [self _hideMenu];
-
- _state.selectedWithoutEdit = NO;
- if (shouldDetectData != [self _shouldDetectText]) {
- [self _update];
- }
- [self _updateIfNeeded];
- [self _updateSelectionView];
- [self performSelector:@selector(_scrollSelectedRangeToVisible) withObject:nil afterDelay:0];
- if ([self.delegate respondsToSelector:@selector(textViewDidBeginEditing:)]) {
- [self.delegate textViewDidBeginEditing:self];
- }
- [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidBeginEditingNotification object:self];
- }
- return become;
- }
- - (BOOL)canResignFirstResponder {
- if (!self.isFirstResponder) return YES;
- if ([self.delegate respondsToSelector:@selector(textViewShouldEndEditing:)]) {
- if (![self.delegate textViewShouldEndEditing:self]) return NO;
- }
- return YES;
- }
- - (BOOL)resignFirstResponder {
- BOOL isFirstResponder = self.isFirstResponder;
- if (!isFirstResponder) return YES;
- BOOL resign = [super resignFirstResponder];
- if (resign) {
- if (_markedTextRange) {
- _markedTextRange = nil;
- [self _parseText];
- [self _setText:[_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)]];
- }
- _state.selectedWithoutEdit = NO;
- if ([self _shouldDetectText]) {
- [self _update];
- }
- [self _endTouchTracking];
- [self _hideMenu];
- [self _updateIfNeeded];
- [self _updateSelectionView];
- [self _restoreInsetsAnimated:YES];
- if ([self.delegate respondsToSelector:@selector(textViewDidEndEditing:)]) {
- [self.delegate textViewDidEndEditing:self];
- }
- [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidEndEditingNotification object:self];
- }
- return resign;
- }
- - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
- /*
- ------------------------------------------------------
- Default menu actions list:
- cut: Cut
- copy: Copy
- select: Select
- selectAll: Select All
- paste: Paste
- delete: Delete
- _promptForReplace: Replace...
- _transliterateChinese: 简⇄繁
- _showTextStyleOptions: 𝐁𝐼𝐔
- _define: Define
- _addShortcut: Add...
- _accessibilitySpeak: Speak
- _accessibilitySpeakLanguageSelection: Speak...
- _accessibilityPauseSpeaking: Pause Speak
- makeTextWritingDirectionRightToLeft: ⇋
- makeTextWritingDirectionLeftToRight: ⇌
-
- ------------------------------------------------------
- Default attribute modifier list:
- toggleBoldface:
- toggleItalics:
- toggleUnderline:
- increaseSize:
- decreaseSize:
- */
-
- if (_selectedTextRange.asRange.length == 0) {
- if (action == @selector(select:) ||
- action == @selector(selectAll:)) {
- return _innerText.length > 0;
- }
- if (action == @selector(paste:)) {
- return [self _isPasteboardContainsValidValue];
- }
- } else {
- if (action == @selector(cut:)) {
- return self.isFirstResponder && self.editable;
- }
- if (action == @selector(copy:)) {
- return YES;
- }
- if (action == @selector(selectAll:)) {
- return _selectedTextRange.asRange.length < _innerText.length;
- }
- if (action == @selector(paste:)) {
- return self.isFirstResponder && self.editable && [self _isPasteboardContainsValidValue];
- }
- NSString *selString = NSStringFromSelector(action);
- if ([selString hasSuffix:@"define:"] && [selString hasPrefix:@"_"]) {
- return [self _getRootViewController] != nil;
- }
- }
- return NO;
- }
- - (void)reloadInputViews {
- [super reloadInputViews];
- if (_markedTextRange) {
- [self unmarkText];
- }
- }
- #pragma mark - Override NSObject(UIResponderStandardEditActions)
- - (void)cut:(id)sender {
- [self _endTouchTracking];
- if (_selectedTextRange.asRange.length == 0) return;
-
- [self _copySelectedTextToPasteboard];
- [self _saveToUndoStack];
- [self _resetRedoStack];
- [self replaceRange:_selectedTextRange withText:@""];
- }
- - (void)copy:(id)sender {
- [self _endTouchTracking];
- [self _copySelectedTextToPasteboard];
- }
- - (void)paste:(id)sender {
- [self _endTouchTracking];
- UIPasteboard *p = [UIPasteboard generalPasteboard];
- NSAttributedString *atr = nil;
-
- if (_allowsPasteAttributedString) {
- atr = p.yy_AttributedString;
- if (atr.length == 0) atr = nil;
- }
- if (!atr && _allowsPasteImage) {
- UIImage *img = nil;
-
- Class cls = NSClassFromString(@"YYImage");
- if (cls) {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wundeclared-selector"
- if (p.yy_GIFData) {
- img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_GIFData withObject:nil];
- }
- if (!img && p.yy_PNGData) {
- img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_PNGData withObject:nil];
- }
- if (!img && p.yy_WEBPData) {
- img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_WEBPData withObject:nil];
- }
- #pragma clang diagnostic pop
- }
-
- if (!img) {
- img = p.image;
- }
- if (!img && p.yy_ImageData) {
- img = [UIImage imageWithData:p.yy_ImageData scale:YYTextScreenScale()];
- }
- if (img && img.size.width > 1 && img.size.height > 1) {
- id content = img;
-
- if (cls) {
- if ([img conformsToProtocol:NSProtocolFromString(@"YYAnimatedImage")]) {
- NSNumber *frameCount = [img valueForKey:@"animatedImageFrameCount"];
- if (frameCount.integerValue > 1) {
- Class viewCls = NSClassFromString(@"YYAnimatedImageView");
- UIImageView *imgView = [(id)viewCls new];
- imgView.image = img;
- imgView.frame = CGRectMake(0, 0, img.size.width, img.size.height);
- if (imgView) {
- content = imgView;
- }
- }
- }
- }
-
- if ([content isKindOfClass:[UIImage class]] && img.images.count > 1) {
- UIImageView *imgView = [UIImageView new];
- imgView.image = img;
- imgView.frame = CGRectMake(0, 0, img.size.width, img.size.height);
- if (imgView) {
- content = imgView;
- }
- }
-
- NSMutableAttributedString *attText = [NSAttributedString yy_attachmentStringWithContent:content contentMode:UIViewContentModeScaleToFill width:img.size.width ascent:img.size.height descent:0];
- NSDictionary *attrs = _typingAttributesHolder.yy_attributes;
- if (attrs) [attText addAttributes:attrs range:NSMakeRange(0, attText.length)];
- atr = attText;
- }
- }
-
- if (atr) {
- NSUInteger endPosition = _selectedTextRange.start.offset + atr.length;
- NSMutableAttributedString *text = _innerText.mutableCopy;
- [text replaceCharactersInRange:_selectedTextRange.asRange withAttributedString:atr];
- self.attributedText = text;
- YYTextPosition *pos = [self _correctedTextPosition:[YYTextPosition positionWithOffset:endPosition]];
- YYTextRange *range = [_innerLayout textRangeByExtendingPosition:pos];
- range = [self _correctedTextRange:range];
- if (range) {
- self.selectedRange = NSMakeRange(range.end.offset, 0);
- }
- } else {
- NSString *string = p.string;
- if (string.length > 0) {
- [self _saveToUndoStack];
- [self _resetRedoStack];
- [self replaceRange:_selectedTextRange withText:string];
- }
- }
- }
- - (void)select:(id)sender {
- [self _endTouchTracking];
-
- if (_selectedTextRange.asRange.length > 0 || _innerText.length == 0) return;
- YYTextRange *newRange = [self _getClosestTokenRangeAtPosition:_selectedTextRange.start];
- if (newRange.asRange.length > 0) {
- [_inputDelegate selectionWillChange:self];
- _selectedTextRange = newRange;
- [_inputDelegate selectionDidChange:self];
- }
-
- [self _updateIfNeeded];
- [self _updateOuterProperties];
- [self _updateSelectionView];
- [self _hideMenu];
- [self _showMenu];
- }
- - (void)selectAll:(id)sender {
- _trackingRange = nil;
- [_inputDelegate selectionWillChange:self];
- _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
- [_inputDelegate selectionDidChange:self];
-
- [self _updateIfNeeded];
- [self _updateOuterProperties];
- [self _updateSelectionView];
- [self _hideMenu];
- [self _showMenu];
- }
- - (void)_define:(id)sender {
- [self _hideMenu];
-
- NSString *string = [_innerText yy_plainTextForRange:_selectedTextRange.asRange];
- if (string.length == 0) return;
- BOOL resign = [self resignFirstResponder];
- if (!resign) return;
-
- UIReferenceLibraryViewController* ref = [[UIReferenceLibraryViewController alloc] initWithTerm:string];
- ref.view.backgroundColor = [UIColor whiteColor];
- [[self _getRootViewController] presentViewController:ref animated:YES completion:^{}];
- }
- #pragma mark - Overrice NSObject(NSKeyValueObservingCustomization)
- + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
- static NSSet *keys = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- keys = [NSSet setWithArray:@[
- @"text",
- @"font",
- @"textColor",
- @"textAlignment",
- @"dataDetectorTypes",
- @"linkTextAttributes",
- @"highlightTextAttributes",
- @"textParser",
- @"attributedText",
- @"textVerticalAlignment",
- @"textContainerInset",
- @"exclusionPaths",
- @"verticalForm",
- @"linePositionModifier",
- @"selectedRange",
- @"typingAttributes"
- ]];
- });
- if ([keys containsObject:key]) {
- return NO;
- }
- return [super automaticallyNotifiesObserversForKey:key];
- }
- #pragma mark - @protocol NSCoding
- - (instancetype)initWithCoder:(NSCoder *)aDecoder {
- self = [super initWithCoder:aDecoder];
- [self _initTextView];
- self.attributedText = [aDecoder decodeObjectForKey:@"attributedText"];
- self.selectedRange = ((NSValue *)[aDecoder decodeObjectForKey:@"selectedRange"]).rangeValue;
- self.textVerticalAlignment = [aDecoder decodeIntegerForKey:@"textVerticalAlignment"];
- self.dataDetectorTypes = [aDecoder decodeIntegerForKey:@"dataDetectorTypes"];
- self.textContainerInset = ((NSValue *)[aDecoder decodeObjectForKey:@"textContainerInset"]).UIEdgeInsetsValue;
- self.exclusionPaths = [aDecoder decodeObjectForKey:@"exclusionPaths"];
- self.verticalForm = [aDecoder decodeBoolForKey:@"verticalForm"];
- return self;
- }
- - (void)encodeWithCoder:(NSCoder *)aCoder {
- [super encodeWithCoder:aCoder];
- [aCoder encodeObject:self.attributedText forKey:@"attributedText"];
- [aCoder encodeObject:[NSValue valueWithRange:self.selectedRange] forKey:@"selectedRange"];
- [aCoder encodeInteger:self.textVerticalAlignment forKey:@"textVerticalAlignment"];
- [aCoder encodeInteger:self.dataDetectorTypes forKey:@"dataDetectorTypes"];
- [aCoder encodeUIEdgeInsets:self.textContainerInset forKey:@"textContainerInset"];
- [aCoder encodeObject:self.exclusionPaths forKey:@"exclusionPaths"];
- [aCoder encodeBool:self.verticalForm forKey:@"verticalForm"];
- }
- #pragma mark - @protocol UIScrollViewDelegate
- - (id<YYTextViewDelegate>)delegate {
- return _outerDelegate;
- }
- - (void)setDelegate:(id<YYTextViewDelegate>)delegate {
- _outerDelegate = delegate;
- }
- - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
- [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
-
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewDidScroll:scrollView];
- }
- }
- - (void)scrollViewDidZoom:(UIScrollView *)scrollView {
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewDidZoom:scrollView];
- }
- }
- - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewWillBeginDragging:scrollView];
- }
- }
- - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
- }
- }
- - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
- if (!decelerate) {
- [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
- }
-
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
- }
- }
- - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewWillBeginDecelerating:scrollView];
- }
- }
- - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
- [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
-
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewDidEndDecelerating:scrollView];
- }
- }
- - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewDidEndScrollingAnimation:scrollView];
- }
- }
- - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
- if ([_outerDelegate respondsToSelector:_cmd]) {
- return [_outerDelegate viewForZoomingInScrollView:scrollView];
- } else {
- return nil;
- }
- }
- - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view{
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewWillBeginZooming:scrollView withView:view];
- }
- }
- - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewDidEndZooming:scrollView withView:view atScale:scale];
- }
- }
- - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
- if ([_outerDelegate respondsToSelector:_cmd]) {
- return [_outerDelegate scrollViewShouldScrollToTop:scrollView];
- }
- return YES;
- }
- - (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
- if ([_outerDelegate respondsToSelector:_cmd]) {
- [_outerDelegate scrollViewDidScrollToTop:scrollView];
- }
- }
- #pragma mark - @protocol YYTextKeyboardObserver
- - (void)keyboardChangedWithTransition:(YYTextKeyboardTransition)transition {
- [self _keyboardChanged];
- }
- #pragma mark - @protocol UIALertViewDelegate
- - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
- NSString *title = [alertView buttonTitleAtIndex:buttonIndex];
- if (title.length == 0) return;
- NSArray *strings = [self _localizedUndoStrings];
- if ([title isEqualToString:strings[1]] || [title isEqualToString:strings[2]]) {
- [self _redo];
- } else if ([title isEqualToString:strings[3]] || [title isEqualToString:strings[4]]) {
- [self _undo];
- }
- [self _restoreFirstResponderAfterUndoAlert];
- }
- #pragma mark - @protocol UIKeyInput
- - (BOOL)hasText {
- return _innerText.length > 0;
- }
- - (void)insertText:(NSString *)text {
- if (text.length == 0) return;
- if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) {
- [self _saveToUndoStack];
- [self _resetRedoStack];
- }
- [self replaceRange:_selectedTextRange withText:text];
- }
- - (void)deleteBackward {
- [self _updateIfNeeded];
- NSRange range = _selectedTextRange.asRange;
- if (range.location == 0 && range.length == 0) return;
- _state.typingAttributesOnce = NO;
-
- // test if there's 'TextBinding' before the caret
- if (!_state.deleteConfirm && range.length == 0 && range.location > 0) {
- NSRange effectiveRange;
- YYTextBinding *binding = [_innerText attribute:YYTextBindingAttributeName atIndex:range.location - 1 longestEffectiveRange:&effectiveRange inRange:NSMakeRange(0, _innerText.length)];
- if (binding && binding.deleteConfirm) {
- _state.deleteConfirm = YES;
- [_inputDelegate selectionWillChange:self];
- _selectedTextRange = [YYTextRange rangeWithRange:effectiveRange];
- _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
- [_inputDelegate selectionDidChange:self];
-
- [self _updateOuterProperties];
- [self _updateSelectionView];
- return;
- }
- }
-
- _state.deleteConfirm = NO;
- if (range.length == 0) {
- YYTextRange *extendRange = [_innerLayout textRangeByExtendingPosition:_selectedTextRange.end inDirection:UITextLayoutDirectionLeft offset:1];
- if ([self _isTextRangeValid:extendRange]) {
- range = extendRange.asRange;
- }
- }
- if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) {
- [self _saveToUndoStack];
- [self _resetRedoStack];
- }
- [self replaceRange:[YYTextRange rangeWithRange:range] withText:@""];
- }
- #pragma mark - @protocol UITextInput
- - (void)setInputDelegate:(id<UITextInputDelegate>)inputDelegate {
- _inputDelegate = inputDelegate;
- }
- - (void)setSelectedTextRange:(YYTextRange *)selectedTextRange {
- if (!selectedTextRange) return;
- selectedTextRange = [self _correctedTextRange:selectedTextRange];
- if ([selectedTextRange isEqual:_selectedTextRange]) return;
- [self _updateIfNeeded];
- [self _endTouchTracking];
- [self _hideMenu];
- _state.deleteConfirm = NO;
- _state.typingAttributesOnce = NO;
-
- [_inputDelegate selectionWillChange:self];
- _selectedTextRange = selectedTextRange;
- _lastTypeRange = _selectedTextRange.asRange;
- [_inputDelegate selectionDidChange:self];
-
- [self _updateOuterProperties];
- [self _updateSelectionView];
-
- if (self.isFirstResponder) {
- [self _scrollRangeToVisible:_selectedTextRange];
- }
- }
- - (void)setMarkedTextStyle:(NSDictionary *)markedTextStyle {
- _markedTextStyle = markedTextStyle.copy;
- }
- /*
- Replace current markedText with the new markedText
- @param markedText New marked text.
- @param selectedRange The range from the '_markedTextRange'
- */
- - (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRange {
- [self _updateIfNeeded];
- [self _endTouchTracking];
- [self _hideMenu];
-
- if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
- NSRange range = _markedTextRange ? _markedTextRange.asRange : NSMakeRange(_selectedTextRange.end.offset, 0);
- BOOL should = [self.delegate textView:self shouldChangeTextInRange:range replacementText:markedText];
- if (!should) return;
- }
-
-
- if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) {
- [self _saveToUndoStack];
- [self _resetRedoStack];
- }
-
- BOOL needApplyHolderAttribute = NO;
- if (_innerText.length > 0 && _markedTextRange) {
- [self _updateAttributesHolder];
- } else {
- needApplyHolderAttribute = YES;
- }
-
- if (_selectedTextRange.asRange.length > 0) {
- [self replaceRange:_selectedTextRange withText:@""];
- }
-
- [_inputDelegate textWillChange:self];
- [_inputDelegate selectionWillChange:self];
-
- if (!markedText) markedText = @"";
- if (_markedTextRange == nil) {
- _markedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_selectedTextRange.end.offset, markedText.length)];
- [_innerText replaceCharactersInRange:NSMakeRange(_selectedTextRange.end.offset, 0) withString:markedText];
- _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_selectedTextRange.start.offset + selectedRange.location, selectedRange.length)];
- } else {
- _markedTextRange = [self _correctedTextRange:_markedTextRange];
- [_innerText replaceCharactersInRange:_markedTextRange.asRange withString:markedText];
- _markedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_markedTextRange.start.offset, markedText.length)];
- _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_markedTextRange.start.offset + selectedRange.location, selectedRange.length)];
- }
-
- _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
- _markedTextRange = [self _correctedTextRange:_markedTextRange];
- if (_markedTextRange.asRange.length == 0) {
- _markedTextRange = nil;
- } else {
- if (needApplyHolderAttribute) {
- [_innerText setAttributes:_typingAttributesHolder.yy_attributes range:_markedTextRange.asRange];
- }
- [_innerText yy_removeDiscontinuousAttributesInRange:_markedTextRange.asRange];
- }
-
- [_inputDelegate selectionDidChange:self];
- [_inputDelegate textDidChange:self];
-
- [self _updateOuterProperties];
- [self _updateLayout];
- [self _updateSelectionView];
- [self _scrollRangeToVisible:_selectedTextRange];
-
- if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
- [self.delegate textViewDidChange:self];
- }
- [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self];
-
- _lastTypeRange = _selectedTextRange.asRange;
- }
- - (void)unmarkText {
- _markedTextRange = nil;
- [self _endTouchTracking];
- [self _hideMenu];
- if ([self _parseText]) _state.needUpdate = YES;
-
- [self _updateIfNeeded];
- [self _updateOuterProperties];
- [self _updateSelectionView];
- [self _scrollRangeToVisible:_selectedTextRange];
- }
- - (void)replaceRange:(YYTextRange *)range withText:(NSString *)text {
- if (!range) return;
- if (!text) text = @"";
- if (range.asRange.length == 0 && text.length == 0) return;
- range = [self _correctedTextRange:range];
-
- if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
- BOOL should = [self.delegate textView:self shouldChangeTextInRange:range.asRange replacementText:text];
- if (!should) return;
- }
-
- BOOL useInnerAttributes = NO;
- if (_innerText.length > 0) {
- if (range.start.offset == 0 && range.end.offset == _innerText.length) {
- if (text.length == 0) {
- NSMutableDictionary *attrs = [_innerText yy_attributesAtIndex:0].mutableCopy;
- [attrs removeObjectsForKeys:[NSMutableAttributedString yy_allDiscontinuousAttributeKeys]];
- _typingAttributesHolder.yy_attributes = attrs;
- }
- }
- } else { // no text
- useInnerAttributes = YES;
- }
- BOOL applyTypingAttributes = NO;
- if (_state.typingAttributesOnce) {
- _state.typingAttributesOnce = NO;
- if (!useInnerAttributes) {
- if (range.asRange.length == 0 && text.length > 0) {
- applyTypingAttributes = YES;
- }
- }
- }
-
- _state.selectedWithoutEdit = NO;
- _state.deleteConfirm = NO;
- [self _endTouchTracking];
- [self _hideMenu];
-
- [self _replaceRange:range withText:text notifyToDelegate:YES];
- if (useInnerAttributes) {
- [_innerText yy_setAttributes:_typingAttributesHolder.yy_attributes];
- } else if (applyTypingAttributes) {
- NSRange newRange = NSMakeRange(range.asRange.location, text.length);
- [_typingAttributesHolder.yy_attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
- [_innerText yy_setAttribute:key value:obj range:newRange];
- }];
- }
- [self _parseText];
- [self _updateOuterProperties];
- [self _update];
-
- if (self.isFirstResponder) {
- [self _scrollRangeToVisible:_selectedTextRange];
- }
-
- if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
- [self.delegate textViewDidChange:self];
- }
- [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self];
-
- _lastTypeRange = _selectedTextRange.asRange;
- }
- - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection forRange:(YYTextRange *)range {
- if (!range) return;
- range = [self _correctedTextRange:range];
- [_innerText yy_setBaseWritingDirection:(NSWritingDirection)writingDirection range:range.asRange];
- [self _commitUpdate];
- }
- - (NSString *)textInRange:(YYTextRange *)range {
- range = [self _correctedTextRange:range];
- if (!range) return @"";
- return [_innerText.string substringWithRange:range.asRange];
- }
- - (UITextWritingDirection)baseWritingDirectionForPosition:(YYTextPosition *)position inDirection:(UITextStorageDirection)direction {
- [self _updateIfNeeded];
- position = [self _correctedTextPosition:position];
- if (!position) return UITextWritingDirectionNatural;
- if (_innerText.length == 0) return UITextWritingDirectionNatural;
- NSUInteger idx = position.offset;
- if (idx == _innerText.length) idx--;
-
- NSDictionary *attrs = [_innerText yy_attributesAtIndex:idx];
- CTParagraphStyleRef paraStyle = (__bridge CFTypeRef)(attrs[NSParagraphStyleAttributeName]);
- if (paraStyle) {
- CTWritingDirection baseWritingDirection;
- if (CTParagraphStyleGetValueForSpecifier(paraStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(CTWritingDirection), &baseWritingDirection)) {
- return (UITextWritingDirection)baseWritingDirection;
- }
- }
-
- return UITextWritingDirectionNatural;
- }
- - (YYTextPosition *)beginningOfDocument {
- return [YYTextPosition positionWithOffset:0];
- }
- - (YYTextPosition *)endOfDocument {
- return [YYTextPosition positionWithOffset:_innerText.length];
- }
- - (YYTextPosition *)positionFromPosition:(YYTextPosition *)position offset:(NSInteger)offset {
- if (offset == 0) return position;
-
- NSUInteger location = position.offset;
- NSInteger newLocation = (NSInteger)location + offset;
- if (newLocation < 0 || newLocation > _innerText.length) return nil;
-
- if (newLocation != 0 && newLocation != _innerText.length) {
- // fix emoji
- [self _updateIfNeeded];
- YYTextRange *extendRange = [_innerLayout textRangeByExtendingPosition:[YYTextPosition positionWithOffset:newLocation]];
- if (extendRange.asRange.length > 0) {
- if (offset < 0) {
- newLocation = extendRange.start.offset;
- } else {
- newLocation = extendRange.end.offset;
- }
- }
- }
-
- YYTextPosition *p = [YYTextPosition positionWithOffset:newLocation];
- return [self _correctedTextPosition:p];
- }
- - (YYTextPosition *)positionFromPosition:(YYTextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset {
- [self _updateIfNeeded];
- YYTextRange *range = [_innerLayout textRangeByExtendingPosition:position inDirection:direction offset:offset];
-
- BOOL forward;
- if (_innerContainer.isVerticalForm) {
- forward = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionDown;
- } else {
- forward = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight;
- }
- if (!forward && offset < 0) {
- forward = -forward;
- }
-
- YYTextPosition *newPosition = forward ? range.end : range.start;
- if (newPosition.offset > _innerText.length) {
- newPosition = [YYTextPosition positionWithOffset:_innerText.length affinity:YYTextAffinityBackward];
- }
-
- return [self _correctedTextPosition:newPosition];
- }
- - (YYTextRange *)textRangeFromPosition:(YYTextPosition *)fromPosition toPosition:(YYTextPosition *)toPosition {
- return [YYTextRange rangeWithStart:fromPosition end:toPosition];
- }
- - (NSComparisonResult)comparePosition:(YYTextPosition *)position toPosition:(YYTextPosition *)other {
- return [position compare:other];
- }
- - (NSInteger)offsetFromPosition:(YYTextPosition *)from toPosition:(YYTextPosition *)toPosition {
- return toPosition.offset - from.offset;
- }
- - (YYTextPosition *)positionWithinRange:(YYTextRange *)range farthestInDirection:(UITextLayoutDirection)direction {
- NSRange nsRange = range.asRange;
- if (direction == UITextLayoutDirectionLeft | direction == UITextLayoutDirectionUp) {
- return [YYTextPosition positionWithOffset:nsRange.location];
- } else {
- return [YYTextPosition positionWithOffset:nsRange.location + nsRange.length affinity:YYTextAffinityBackward];
- }
- }
- - (YYTextRange *)characterRangeByExtendingPosition:(YYTextPosition *)position inDirection:(UITextLayoutDirection)direction {
- [self _updateIfNeeded];
- YYTextRange *range = [_innerLayout textRangeByExtendingPosition:position inDirection:direction offset:1];
- return [self _correctedTextRange:range];
- }
- - (YYTextPosition *)closestPositionToPoint:(CGPoint)point {
- [self _updateIfNeeded];
- point = [self _convertPointToLayout:point];
- YYTextPosition *position = [_innerLayout closestPositionToPoint:point];
- return [self _correctedTextPosition:position];
- }
- - (YYTextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(YYTextRange *)range {
- YYTextPosition *pos = (id)[self closestPositionToPoint:point];
- if (!pos) return nil;
-
- range = [self _correctedTextRange:range];
- if ([pos compare:range.start] == NSOrderedAscending) {
- pos = range.start;
- } else if ([pos compare:range.end] == NSOrderedDescending) {
- pos = range.end;
- }
- return pos;
- }
- - (YYTextRange *)characterRangeAtPoint:(CGPoint)point {
- [self _updateIfNeeded];
- point = [self _convertPointToLayout:point];
- YYTextRange *r = [_innerLayout closestTextRangeAtPoint:point];
- return [self _correctedTextRange:r];
- }
- - (CGRect)firstRectForRange:(YYTextRange *)range {
- [self _updateIfNeeded];
- CGRect rect = [_innerLayout firstRectForRange:range];
- if (CGRectIsNull(rect)) rect = CGRectZero;
- return [self _convertRectFromLayout:rect];
- }
- - (CGRect)caretRectForPosition:(YYTextPosition *)position {
- [self _updateIfNeeded];
- CGRect caretRect = [_innerLayout caretRectForPosition:position];
- if (!CGRectIsNull(caretRect)) {
- caretRect = [self _convertRectFromLayout:caretRect];
- caretRect = CGRectStandardize(caretRect);
- if (_verticalForm) {
- if (caretRect.size.height == 0) {
- caretRect.size.height = 2;
- caretRect.origin.y -= 2 * 0.5;
- }
- if (caretRect.origin.y < 0) {
- caretRect.origin.y = 0;
- } else if (caretRect.origin.y + caretRect.size.height > self.bounds.size.height) {
- caretRect.origin.y = self.bounds.size.height - caretRect.size.height;
- }
- } else {
- if (caretRect.size.width == 0) {
- caretRect.size.width = 2;
- caretRect.origin.x -= 2 * 0.5;
- }
- if (caretRect.origin.x < 0) {
- caretRect.origin.x = 0;
- } else if (caretRect.origin.x + caretRect.size.width > self.bounds.size.width) {
- caretRect.origin.x = self.bounds.size.width - caretRect.size.width;
- }
- }
- return YYTextCGRectPixelRound(caretRect);
- }
- return CGRectZero;
- }
- - (NSArray *)selectionRectsForRange:(YYTextRange *)range {
- [self _updateIfNeeded];
- NSArray *rects = [_innerLayout selectionRectsForRange:range];
- [rects enumerateObjectsUsingBlock:^(YYTextSelectionRect *rect, NSUInteger idx, BOOL *stop) {
- rect.rect = [self _convertRectFromLayout:rect.rect];
- }];
- return rects;
- }
- #pragma mark - @protocol UITextInput optional
- - (UITextStorageDirection)selectionAffinity {
- if (_selectedTextRange.end.affinity == YYTextAffinityForward) {
- return UITextStorageDirectionForward;
- } else {
- return UITextStorageDirectionBackward;
- }
- }
- - (void)setSelectionAffinity:(UITextStorageDirection)selectionAffinity {
- _selectedTextRange = [YYTextRange rangeWithRange:_selectedTextRange.asRange affinity:selectionAffinity == UITextStorageDirectionForward ? YYTextAffinityForward : YYTextAffinityBackward];
- [self _updateSelectionView];
- }
- - (NSDictionary *)textStylingAtPosition:(YYTextPosition *)position inDirection:(UITextStorageDirection)direction {
- if (!position) return nil;
- if (_innerText.length == 0) return _typingAttributesHolder.yy_attributes;
- NSDictionary *attrs = nil;
- if (0 <= position.offset && position.offset <= _innerText.length) {
- NSUInteger ofs = position.offset;
- if (position.offset == _innerText.length ||
- direction == UITextStorageDirectionBackward) {
- ofs--;
- }
- attrs = [_innerText attributesAtIndex:ofs effectiveRange:NULL];
- }
- return attrs;
- }
- - (YYTextPosition *)positionWithinRange:(YYTextRange *)range atCharacterOffset:(NSInteger)offset {
- if (!range) return nil;
- if (offset < range.start.offset || offset > range.end.offset) return nil;
- if (offset == range.start.offset) return range.start;
- else if (offset == range.end.offset) return range.end;
- else return [YYTextPosition positionWithOffset:offset];
- }
- - (NSInteger)characterOffsetOfPosition:(YYTextPosition *)position withinRange:(YYTextRange *)range {
- return position ? position.offset : NSNotFound;
- }
- @end
- @interface YYTextView(IBInspectableProperties)
- @end
- @implementation YYTextView(IBInspectableProperties)
- - (BOOL)fontIsBold_:(UIFont *)font {
- if (![font respondsToSelector:@selector(fontDescriptor)]) return NO;
- return (font.fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) > 0;
- }
- - (UIFont *)boldFont_:(UIFont *)font {
- if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
- return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize];
- }
- - (UIFont *)normalFont_:(UIFont *)font {
- if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
- return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:0] size:font.pointSize];
- }
- - (void)setFontName_:(NSString *)fontName {
- if (!fontName) return;
- UIFont *font = self.font;
- if (!font) font = [self _defaultFont];
- if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
- font = [UIFont systemFontOfSize:font.pointSize];
- } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
- font = [UIFont boldSystemFontOfSize:font.pointSize];
- } else {
- if ([self fontIsBold_:font] && ([fontName.lowercaseString rangeOfString:@"bold"].location == NSNotFound)) {
- font = [UIFont fontWithName:fontName size:font.pointSize];
- font = [self boldFont_:font];
- } else {
- font = [UIFont fontWithName:fontName size:font.pointSize];
- }
- }
- if (font) self.font = font;
- }
- - (void)setFontSize_:(CGFloat)fontSize {
- if (fontSize <= 0) return;
- UIFont *font = self.font;
- if (!font) font = [self _defaultFont];
- if (!font) font = [self _defaultFont];
- font = [font fontWithSize:fontSize];
- if (font) self.font = font;
- }
- - (void)setFontIsBold_:(BOOL)fontBold {
- UIFont *font = self.font;
- if (!font) font = [self _defaultFont];
- if ([self fontIsBold_:font] == fontBold) return;
- if (fontBold) {
- font = [self boldFont_:font];
- } else {
- font = [self normalFont_:font];
- }
- if (font) self.font = font;
- }
- - (void)setPlaceholderFontName_:(NSString *)fontName {
- if (!fontName) return;
- UIFont *font = self.placeholderFont;
- if (!font) font = [self _defaultFont];
- if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
- font = [UIFont systemFontOfSize:font.pointSize];
- } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
- font = [UIFont boldSystemFontOfSize:font.pointSize];
- } else {
- if ([self fontIsBold_:font] && ([fontName.lowercaseString rangeOfString:@"bold"].location == NSNotFound)) {
- font = [UIFont fontWithName:fontName size:font.pointSize];
- font = [self boldFont_:font];
- } else {
- font = [UIFont fontWithName:fontName size:font.pointSize];
- }
- }
- if (font) self.placeholderFont = font;
- }
- - (void)setPlaceholderFontSize_:(CGFloat)fontSize {
- if (fontSize <= 0) return;
- UIFont *font = self.placeholderFont;
- if (!font) font = [self _defaultFont];
- font = [font fontWithSize:fontSize];
- if (font) self.placeholderFont = font;
- }
- - (void)setPlaceholderFontIsBold_:(BOOL)fontBold {
- UIFont *font = self.placeholderFont;
- if (!font) font = [self _defaultFont];
- if ([self fontIsBold_:font] == fontBold) return;
- if (fontBold) {
- font = [self boldFont_:font];
- } else {
- font = [self normalFont_:font];
- }
- if (font) self.placeholderFont = font;
- }
- - (void)setInsetTop_:(CGFloat)textInsetTop {
- UIEdgeInsets insets = self.textContainerInset;
- insets.top = textInsetTop;
- self.textContainerInset = insets;
- }
- - (void)setInsetBottom_:(CGFloat)textInsetBottom {
- UIEdgeInsets insets = self.textContainerInset;
- insets.bottom = textInsetBottom;
- self.textContainerInset = insets;
- }
- - (void)setInsetLeft_:(CGFloat)textInsetLeft {
- UIEdgeInsets insets = self.textContainerInset;
- insets.left = textInsetLeft;
- self.textContainerInset = insets;
-
- }
- - (void)setInsetRight_:(CGFloat)textInsetRight {
- UIEdgeInsets insets = self.textContainerInset;
- insets.right = textInsetRight;
- self.textContainerInset = insets;
- }
- - (void)setDebugEnabled_:(BOOL)enabled {
- if (!enabled) {
- self.debugOption = nil;
- } else {
- YYTextDebugOption *debugOption = [YYTextDebugOption new];
- debugOption.baselineColor = [UIColor redColor];
- debugOption.CTFrameBorderColor = [UIColor redColor];
- debugOption.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180];
- debugOption.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200];
- self.debugOption = debugOption;
- }
- }
- @end
|