123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792 |
- //
- // YYTextView.m
- // YYKit <https://github.com/ibireme/YYKit>
- //
- // 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 "YYKitMacro.h"
- #import "YYTextInput.h"
- #import "YYTextContainerView.h"
- #import "YYTextSelectionView.h"
- #import "YYTextMagnifier.h"
- #import "YYTextEffectWindow.h"
- #import "YYTextKeyboardManager.h"
- #import "YYTextUtilities.h"
- #import "YYCGUtilities.h"
- #import "YYTransaction.h"
- #import "YYWeakProxy.h"
- #import "UIView+YYAdd.h"
- #import "NSAttributedString+YYText.h"
- #import "UIPasteboard+YYText.h"
- #import "UIDevice+YYAdd.h"
- #import "UIApplication+YYAdd.h"
- #import "YYImage.h"
- #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;
- [[YYTransaction 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 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 setAttributes:_typingAttributesHolder.attributes]; // add for empty text caret
- }
- if (_selectedTextRange.end.offset == _innerText.length) {
- [_typingAttributesHolder.attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
- [text 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;
- [[YYTransaction 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 ([UIApplication isAppExtension]) 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 ([UIApplication isAppExtension]) 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 = YY_CLAMP(magPoint.x, CGRectGetMinX(lineRect), CGRectGetMaxX(lineRect));
- } else {
- magPoint.y = YY_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 ([UIApplication isAppExtension]) return;
-
- if (_state.showingMagnifierCaret) {
- [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierCaret];
- }
- if (_state.showingMagnifierRanged) {
- [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierRanged];
- }
- }
- /// Hide the `_magnifierCaret` and `_magnifierRanged`.
- - (void)_hideMagnifier {
- if ([UIApplication isAppExtension]) 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 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:[YYWeakProxy 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:[YYWeakProxy 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:[YYWeakProxy 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 ([UIApplication isAppExtension]) return;
-
- CGPoint origin = [self 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 = YY_CLAMP(magPoint.x, line.left, line.right);
- return magPoint.x - line.position.x + kMagnifierRangedPopoverOffset;
- } else {
- magPoint.y = YY_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.attributedString.length > 0) {
- if (_allowsPasteAttributedString) {
- return YES;
- }
- }
- if (pasteboard.image || pasteboard.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].attributedString = text;
- }
- } else {
- NSString *string = [_innerText 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 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 attributesAtIndex:index];
- if (!attributes) attributes = @{};
- _typingAttributesHolder.attributes = attributes;
- [_typingAttributesHolder 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.paragraphStyle;
- if (!style) style = _typingAttributesHolder.paragraphStyle;
- if (!style) style = [NSParagraphStyle defaultParagraphStyle];
-
- UIFont *font = _innerText.font;
- if (!font) font = _typingAttributesHolder.font;
- if (!font) font = [self _defaultFont];
-
- UIColor *color = _innerText.color;
- if (!color) color = _typingAttributesHolder.color;
- if (!color) color = [UIColor blackColor];
-
- [self _setText:[_innerText plainTextForRange:NSMakeRange(0, _innerText.length)]];
- [self _setFont:font];
- [self _setTextColor:color];
- [self _setTextAlignment:style.alignment];
- [self _setSelectedRange:_selectedTextRange.asRange];
- [self _setTypingAttributes:_typingAttributesHolder.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 setTextHighlight:highlight range:result.range];
- }
- if (_linkTextAttributes.count) {
- [_linkTextAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
- [text 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 = [UIApplication sharedExtensionApplication];
- if (!ctrl) ctrl = app.keyWindow.rootViewController;
- if (!ctrl) ctrl = [app.windows.firstObject rootViewController];
- if (!ctrl) ctrl = self.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 containsString:@"_"]) {
- 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.font = font;
- _innerText.font = font;
- [self _resetUndoAndRedoStack];
- [self _commitUpdate];
- }
- - (void)setTextColor:(UIColor *)textColor {
- if (_textColor == textColor || [_textColor isEqual:textColor]) return;
- [self _setTextColor:textColor];
-
- _state.typingAttributesOnce = NO;
- _typingAttributesHolder.color = textColor;
- _innerText.color = textColor;
- [self _resetUndoAndRedoStack];
- [self _commitUpdate];
- }
- - (void)setTextAlignment:(NSTextAlignment)textAlignment {
- if (_textAlignment == textAlignment) return;
- [self _setTextAlignment:textAlignment];
-
- _typingAttributesHolder.alignment = textAlignment;
- _innerText.alignment = textAlignment;
- [self _resetUndoAndRedoStack];
- [self _commitUpdate];
- }
- - (void)setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes {
- if (_dataDetectorTypes == dataDetectorTypes) return;
- [self _setDataDetectorTypes:dataDetectorTypes];
- NSTextCheckingType type = NSTextCheckingTypeFromUIDataDetectorType(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 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.attributes = [_innerText 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).font = _placeholderFont;
- ((NSMutableAttributedString *)_placeholderAttributedText).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.font = _placeholderFont;
- atr.color = _placeholderTextColor;
- _placeholderAttributedText = atr;
- }
- }
- _placeholderText = [_placeholderAttributedText plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)];
- [self _commitPlaceholderUpdate];
- }
- - (void)setPlaceholderFont:(UIFont *)placeholderFont {
- _placeholderFont = placeholderFont;
- ((NSMutableAttributedString *)_placeholderAttributedText).font = _placeholderFont;
- [self _commitPlaceholderUpdate];
- }
- - (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor {
- _placeholderTextColor = placeholderTextColor;
- ((NSMutableAttributedString *)_placeholderAttributedText).color = _placeholderTextColor;
- [self _commitPlaceholderUpdate];
- }
- - (void)setPlaceholderAttributedText:(NSAttributedString *)placeholderAttributedText {
- _placeholderAttributedText = placeholderAttributedText.mutableCopy;
- _placeholderText = [_placeholderAttributedText plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)];
- _placeholderFont = _placeholderAttributedText.font;
- _placeholderTextColor = _placeholderAttributedText.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 (![UIApplication isAppExtension]) {
- #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 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.attributedString;
- if (atr.length == 0) atr = nil;
- }
- if (!atr && _allowsPasteImage) {
- UIImage *img = nil;
- if (p.GIFData) {
- img = [YYImage imageWithData:p.GIFData scale:kScreenScale];
- }
- if (!img && p.PNGData) {
- img = [YYImage imageWithData:p.PNGData scale:kScreenScale];
- }
- if (!img && p.WEBPData) {
- img = [YYImage imageWithData:p.WEBPData scale:kScreenScale];
- }
- if (!img) {
- img = p.image;
- }
- if (!img && p.imageData) {
- img = [UIImage imageWithData:p.imageData scale:kScreenScale];
- }
- if (img && img.size.width > 1 && img.size.height > 1) {
- id content = img;
- if ([img conformsToProtocol:@protocol(YYAnimatedImage)]) {
- id<YYAnimatedImage> ani = (id)img;
- if (ani.animatedImageFrameCount > 1) {
- YYAnimatedImageView *aniView = [[YYAnimatedImageView alloc] initWithImage:img];
- if (aniView) {
- content = aniView;
- }
- }
- }
-
- 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 attachmentStringWithContent:content contentMode:UIViewContentModeScaleToFill width:img.size.width ascent:img.size.height descent:0];
- NSDictionary *attrs = _typingAttributesHolder.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 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.attributes range:_markedTextRange.asRange];
- }
- [_innerText 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 attributesAtIndex:0].mutableCopy;
- [attrs removeObjectsForKeys:[NSMutableAttributedString allDiscontinuousAttributeKeys]];
- _typingAttributesHolder.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 setAttributes:_typingAttributesHolder.attributes];
- } else if (applyTypingAttributes) {
- NSRange newRange = NSMakeRange(range.asRange.location, text.length);
- [_typingAttributesHolder.attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
- [_innerText 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 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 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 CGRectPixelRound(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.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 containsString:@"bold"]) {
- 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 containsString:@"bold"]) {
- 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
|